public inbox for [email protected]help / color / mirror / Atom feed
[PATCH 18/18] doc review for pg_stat_progress_create_index 36+ messages / 8 participants [nested] [flat]
* [PATCH 18/18] doc review for pg_stat_progress_create_index @ 2021-02-10 23:19 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 36+ messages in thread From: Justin Pryzby @ 2021-02-10 23:19 UTC (permalink / raw) ab0dfc961b6a821f23d9c40c723d11380ce195a6 should backpatch to v13 --- doc/src/sgml/monitoring.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index c602ee4427..16eb1d9e9c 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -5725,7 +5725,7 @@ SELECT pg_stat_get_backend_pid(s.backendid) AS pid, </para> <para> When creating an index on a partitioned table, this column is set to - the number of partitions on which the index has been completed. + the number of partitions on which the index has been created. </para></entry> </row> </tbody> -- 2.17.0 --lc9FT7cWel8HagAv-- ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-07 13:59 Peter Eisentraut <[email protected]> 0 siblings, 1 reply; 36+ messages in thread From: Peter Eisentraut @ 2025-04-07 13:59 UTC (permalink / raw) To: Jacob Champion <[email protected]>; Christoph Berg <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]> On 05.04.25 02:27, Jacob Champion wrote: > On Tue, Apr 1, 2025 at 3:40 PM Jacob Champion > <[email protected]> wrote: >> Maybe a better idea would be to ship an SONAME of >> `libpq-oauth.so.0.<major>`, without any symlinks, so that there's >> never any ambiguity about which module belongs with which libpq. > > While I was looking into this I found that Debian's going to use the > existence of an SONAME to check other things, which I assume will make > Christoph's life harder. I have switched over to > 'libpq-oauth-<major>.so', without any SONAME or symlinks. Yes, this is correct. We want a shared module, not a shared library, in meson parlance. But the installation directory of a shared module should not be directly libdir. That is reserved for libraries that you can link at build-time. Here are some examples I found of other packages that have a library that itself has plugins: https://packages.debian.org/bookworm/amd64/libaspell15/filelist https://packages.debian.org/bookworm/amd64/libkrb5-3/filelist https://packages.debian.org/bookworm/amd64/libmagickcore-6.q16-6/filelist > v2 simplifies quite a few things and breaks out the new duplicated > code into its own file. I pared down the exports from libpq, by having > it push the pg_g_threadlock pointer directly into the module when > needed. I think a future improvement would be to combine the dlopen > with the libcurl initialization, so that everything is done exactly > once and the module doesn't need to know about threadlocks at all. Looks mostly ok. I need the following patch to get it to build on macOS: diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile index 461c44b59c1..d5469ca0e11 100644 --- a/src/interfaces/libpq-oauth/Makefile +++ b/src/interfaces/libpq-oauth/Makefile @@ -28,8 +28,9 @@ OBJS = \ oauth-utils.o SHLIB_LINK_INTERNAL = $(libpq_pgport_shlib) -SHLIB_LINK = -lcurl +SHLIB_LINK = -lcurl $(filter -lintl, $(LIBS)) SHLIB_PREREQS = submake-libpq +BE_DLLLIBS = SHLIB_EXPORTS = exports.txt (The second change is not strictly required, but it disables the use of -bundle_loader postgres, since this is not a backend-loadable module.) I don't know whether we need an exports file for libpq-oauth. The other shared modules don't provide an export file, and I'm not sure whether this is even supported for shared modules. I guess it doesn't hurt? The PKG_CONFIG_REQUIRES_PRIVATE setting in libpq-oauth/Makefile is meaningless and can be removed. In fe-auth-oauth.c, I note that dlerror() is not necessarily thread-safe. Since there isn't really an alternative, I guess it's ok to leave it like that, but I figured it should be mentioned. > i18n is still not working correctly on my machine. I've gotten `make > init-po` to put the files into the right places now, but if I fake a > .po file and install the generated .mo, the translations still don't > seem to be found at runtime. Is anyone able to take a quick look to > see if I'm missing something obvious? Not sure, the code looks correct at first glance. However, you could also just keep the libpq-oauth strings in the libpq catalog. There isn't really a need to make a separate one, since the versions you end up installing are locked to each other. So you could for example in libpq's nls.mk just add ../libpq-oauth/oauth-curl.c etc. to the files. Maybe it would also make sense to make libpq-oauth a subdirectory of the libpq directory instead of a peer. ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-07 14:43 Andres Freund <[email protected]> parent: Peter Eisentraut <[email protected]> 0 siblings, 2 replies; 36+ messages in thread From: Andres Freund @ 2025-04-07 14:43 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Jacob Champion <[email protected]>; Christoph Berg <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]> Hi, On 2025-04-07 15:59:19 +0200, Peter Eisentraut wrote: > On 05.04.25 02:27, Jacob Champion wrote: > > On Tue, Apr 1, 2025 at 3:40 PM Jacob Champion > > <[email protected]> wrote: > > > Maybe a better idea would be to ship an SONAME of > > > `libpq-oauth.so.0.<major>`, without any symlinks, so that there's > > > never any ambiguity about which module belongs with which libpq. > > > > While I was looking into this I found that Debian's going to use the > > existence of an SONAME to check other things, which I assume will make > > Christoph's life harder. I have switched over to > > 'libpq-oauth-<major>.so', without any SONAME or symlinks. > > Yes, this is correct. We want a shared module, not a shared library, in > meson parlance. It's not entirely obvious to me that we do want that. There recently was a breakage of building with PG on macos with meson, due to the meson folks implementing a feature request to move away from using bundles, as 1) bundles apparently aren't supported on iOS 2) there apparently aren't any restrictions left that require using bundles, and there haven't been for a while. They've now reverted these changes, due to the postgres build failures that caused as well as recognizing they probably moved too fast, but the iOS portion seems like it could be relevant for us? Afaict this library doesn't have unresolved symbols, due to just linking to libpq. So I don't think we really need this to be a shared module? > But the installation directory of a shared module should not be directly > libdir. Agreed. However, it seems like relocatability would be much more important for something like libpq than server modules... Particularly on windows it'll often just be shipped alongside the executable - which won't work if we load from pklibdir or such. > I don't know whether we need an exports file for libpq-oauth. The other > shared modules don't provide an export file, and I'm not sure whether this > is even supported for shared modules. I guess it doesn't hurt? It does seem just using PGDLLEXPORT would suffice here. > The PKG_CONFIG_REQUIRES_PRIVATE setting in libpq-oauth/Makefile is > meaningless and can be removed. > > In fe-auth-oauth.c, I note that dlerror() is not necessarily thread-safe. I sometimes really really hate posix. Greetings, Andres Freund ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-07 16:38 Peter Eisentraut <[email protected]> parent: Andres Freund <[email protected]> 1 sibling, 1 reply; 36+ messages in thread From: Peter Eisentraut @ 2025-04-07 16:38 UTC (permalink / raw) To: Andres Freund <[email protected]>; +Cc: Jacob Champion <[email protected]>; Christoph Berg <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]> On 07.04.25 16:43, Andres Freund wrote: >>> While I was looking into this I found that Debian's going to use the >>> existence of an SONAME to check other things, which I assume will make >>> Christoph's life harder. I have switched over to >>> 'libpq-oauth-<major>.so', without any SONAME or symlinks. >> Yes, this is correct. We want a shared module, not a shared library, in >> meson parlance. > It's not entirely obvious to me that we do want that. > > There recently was a breakage of building with PG on macos with meson, due to > the meson folks implementing a feature request to move away from using > bundles, as > 1) bundles apparently aren't supported on iOS > 2) there apparently aren't any restrictions left that require using bundles, > and there haven't been for a while. > > They've now reverted these changes, due to the postgres build failures that > caused as well as recognizing they probably moved too fast, but the iOS > portion seems like it could be relevant for us? Um, interesting. AFAICT, the change you mention was reverted from the 1.7 branch because it was accidentally backpatched, but it remains in master. (For those just catching up: https://github.com/mesonbuild/meson/issues/14240 https://github.com/mesonbuild/meson/pull/14340 https://github.com/mesonbuild/meson/commit/fa3f7e10b47d1f2f438f216f6c44f56076a01bfc ) Overall, this seems like a good idea, as it removes a historical platform-specific particularity. (I found a historical analysis at <https://stackoverflow.com/questions/2339679/;.) But it does break existing users that add -bundle_loader, because -bundle_loader only works with -bundle and is rejected with -dynamiclib. To test, I patched the makefiles to use -dynamiclib instead of -bundle, which also required removing -bundle_loader, and it also required adding -Wl,-undefined,dynamic_lookup. This built correctly and generally worked. But then you also run into a new variant of this issue: https://www.postgresql.org/message-id/[email protected] Because there is no -bundle_loader, the symbol search order appears to be different, and so hash_search() gets found in the OS library first. So this is all going to be a mess at some point sooner or later. :-( > Afaict this library doesn't have unresolved symbols, due to just linking to > libpq. So I don't think we really need this to be a shared module? Apart from the hard distinction on macOS, in terms of the build system, the distinction between "library" and "module" is mainly whether the resulting library gets a soname, version symlinks, and what directory it is installed in, so in that sense the discussion so far indicates that it should be a module. I suppose on macOS we could link it like a library and install it like a module, but that would effectively create a third category, and I don't see why that would be worth it. ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-07 16:41 Jacob Champion <[email protected]> parent: Andres Freund <[email protected]> 1 sibling, 3 replies; 36+ messages in thread From: Jacob Champion @ 2025-04-07 16:41 UTC (permalink / raw) To: Andres Freund <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Christoph Berg <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]> Hi all, Thanks for all the feedback! I'll combine them all into one email: On Mon, Apr 7, 2025 at 6:59 AM Peter Eisentraut <[email protected]> wrote: > Looks mostly ok. I need the following patch to get it to build on macOS: > [...] > (The second change is not strictly required, but it disables the use of > -bundle_loader postgres, since this is not a backend-loadable module.) Hm, okay. I'll take a closer look at this, thanks. > The PKG_CONFIG_REQUIRES_PRIVATE setting in libpq-oauth/Makefile is > meaningless and can be removed. Ah, right. Will do. > In fe-auth-oauth.c, I note that dlerror() is not necessarily > thread-safe. Since there isn't really an alternative, I guess it's ok > to leave it like that, but I figured it should be mentioned. Yeah. The XXX comments there are a reminder to me to lock the stderr printing behind debug mode, since I hope most non-packaging people are not going to be troubleshooting load-time errors. But see the threadlock discussions below. > Not sure, the code looks correct at first glance. However, you could > also just keep the libpq-oauth strings in the libpq catalog. There > isn't really a need to make a separate one, since the versions you end > up installing are locked to each other. So you could for example in > libpq's nls.mk just add > > ../libpq-oauth/oauth-curl.c > > etc. to the files. Oh, that's an interesting idea. Thanks, I'll give it a try. > Maybe it would also make sense to make libpq-oauth a subdirectory of the > libpq directory instead of a peer. Works for me. On Mon, Apr 7, 2025 at 7:21 AM Andres Freund <[email protected]> wrote: > On 2025-04-04 17:27:46 -0700, Jacob Champion wrote: > > +This module ABI is an internal implementation detail, so it's subject to change > > +without warning, even during minor releases (however unlikely). The compiled > > +version of libpq-oauth should always match the compiled version of libpq. > > Shouldn't we then include the *minor* version in the soname? No objection here. > I think otherwise > we run into the danger of the wrong library version being loaded in some > cases. Imagine a program being told with libpq to use via rpath. But then we > load the oauth module via a dlopen without a specified path - it'll just > search the global library locations. Ah, you mean if the RPATH'd build doesn't have a flow, but the globally installed one (with a different ABI) does? Yeah, that would be a problem. > Which actually makes me wonder if we ought to instead load the library from a > specific location... We could hardcode the disk location for version 1, I guess. I kind of liked giving packagers the flexibility they're used to having from the ld.so architecture, though. See below. > > +# src/interfaces/libpq-oauth/exports.txt > > +pg_fe_run_oauth_flow 1 > > +pg_fe_cleanup_oauth_flow 2 > > +pg_g_threadlock 3 > > The pg_g_threadlock thing seems pretty ugly. Can't we just pass that to the > relevant functions? We can do it however we want, honestly, especially since the ABI isn't public/stable. I chose this way just to ease review. > But more fundamentally, are we actually using > pg_g_threadlock anywhere? I removed all the releant code and the tests still > seem to pass? If you have an older Curl installation, it is used. Newer libcurls don't need it. A future simplification could be to pull the use of the threadlock back into libpq, and have it perform a one-time dlopen-plus-Curl-initialization under the lock... That would also get rid of the dlerror() thread safety problems. But that's an awful lot of moving parts under a mutex, which makes me a little nervous. > > diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build > > +if not libcurl.found() or host_system == 'windows' > > + subdir_done() > > +endif > > Why does this not work on windows? I don't see similar code in the removed > lines? The Device Authorization flow is not currently implemented on Windows. On Mon, Apr 7, 2025 at 7:43 AM Andres Freund <[email protected]> wrote: > > Yes, this is correct. We want a shared module, not a shared library, in > > meson parlance. > > It's not entirely obvious to me that we do want that. > > There recently was a breakage of building with PG on macos with meson, due to > the meson folks implementing a feature request to move away from using > bundles, as > 1) bundles apparently aren't supported on iOS > 2) there apparently aren't any restrictions left that require using bundles, > and there haven't been for a while. Could you explain how this is related to .app bundles? I thought I was just building a standard dylib. > Afaict this library doesn't have unresolved symbols, due to just linking to > libpq. So I don't think we really need this to be a shared module? Correct, no unresolved symbols. My naive understanding was that distros were going to impose restrictions on an SONAME'd library that we may not want to deal with. > > But the installation directory of a shared module should not be directly > > libdir. > > Agreed. However, it seems like relocatability would be much more important for > something like libpq than server modules... Particularly on windows it'll > often just be shipped alongside the executable - which won't work if we load > from pklibdir or such. Yeah, I really like the simplicity of "use the standard runtime loader, just on-demand." Seems more friendly to the ecosystem. Are there technical downsides of putting it into $libdir? I understand there are "people" downsides, since we don't really want to signal that this is a publicly linkable thing... but surely if you go through the process of linking our library (which has no SONAME, includes the major/minor version in its -l option, and has no pkgconfig) and shoving a private pointer to a threadlock into it, you can keep all the pieces when they break? > > I don't know whether we need an exports file for libpq-oauth. The other > > shared modules don't provide an export file, and I'm not sure whether this > > is even supported for shared modules. I guess it doesn't hurt? > > It does seem just using PGDLLEXPORT would suffice here. My motivation was to strictly identify the ABI that we intend libpq to use, to try to future-proof things for everybody. Especially since we're duplicating functions from libpq that we'd rather not be. (The use of RTLD_LOCAL maybe makes that more of a belt-and-suspenders thing.) Are there any downsides to the exports file? Thanks, --Jacob ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-07 16:52 Andres Freund <[email protected]> parent: Peter Eisentraut <[email protected]> 0 siblings, 0 replies; 36+ messages in thread From: Andres Freund @ 2025-04-07 16:52 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Jacob Champion <[email protected]>; Christoph Berg <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]> Hi, On 2025-04-07 18:38:19 +0200, Peter Eisentraut wrote: > On 07.04.25 16:43, Andres Freund wrote: > > There recently was a breakage of building with PG on macos with meson, due to > > the meson folks implementing a feature request to move away from using > > bundles, as > > 1) bundles apparently aren't supported on iOS > > 2) there apparently aren't any restrictions left that require using bundles, > > and there haven't been for a while. > > > > They've now reverted these changes, due to the postgres build failures that > > caused as well as recognizing they probably moved too fast, but the iOS > > portion seems like it could be relevant for us? > > Um, interesting. AFAICT, the change you mention was reverted from the 1.7 > branch because it was accidentally backpatched, but it remains in master. I think the plan is to either redesign it in master or to revert it. > (For those just catching up: > > https://github.com/mesonbuild/meson/issues/14240 > https://github.com/mesonbuild/meson/pull/14340 > https://github.com/mesonbuild/meson/commit/fa3f7e10b47d1f2f438f216f6c44f56076a01bfc > ) > > Overall, this seems like a good idea, as it removes a historical > platform-specific particularity. (I found a historical analysis at > <https://stackoverflow.com/questions/2339679/;.) > > But it does break existing users that add -bundle_loader, because > -bundle_loader only works with -bundle and is rejected with -dynamiclib. Seems hard to imagine that somebody would inject -bundle_loader separately from src/makefiles/Makefile.darwin? > To test, I patched the makefiles to use -dynamiclib instead of -bundle, > which also required removing -bundle_loader, and it also required adding > -Wl,-undefined,dynamic_lookup. This built correctly and generally worked. > > But then you also run into a new variant of this issue: > > https://www.postgresql.org/message-id/[email protected] > > Because there is no -bundle_loader, the symbol search order appears to be > different, and so hash_search() gets found in the OS library first. > > So this is all going to be a mess at some point sooner or later. :-( Yikes, that is depressing / scary. I wonder if we ought to rename our hash_search with some macro magic or such regardless of using -bundle or not. > > Afaict this library doesn't have unresolved symbols, due to just linking to > > libpq. So I don't think we really need this to be a shared module? > > Apart from the hard distinction on macOS, in terms of the build system, the > distinction between "library" and "module" is mainly whether the resulting > library gets a soname, version symlinks, and what directory it is installed > in, so in that sense the discussion so far indicates that it should be a > module. I don't think that happens if you don't specify a soname etc. And we'd need to adjust the install dir either way, I think? > I suppose on macOS we could link it like a library and install it > like a module, but that would effectively create a third category, and I > don't see why that would be worth it. I think there are postgres clients for iphone, not sure if they use libpq. Today libpq might actually cross-build successfully for iOS [1]. But if we use shared_module() that won't be the case for libpq-oauth. Anyway, I don't have a strong position on this, I just wanted to bring it up for consideration. Greetings, Andres Freund [1] I couldn't immediately quickly figure out how to install additional SDKs on the commandline on macos an then gave up before attaching a monitor to my mac mini. ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-07 17:05 Andres Freund <[email protected]> parent: Jacob Champion <[email protected]> 2 siblings, 1 reply; 36+ messages in thread From: Andres Freund @ 2025-04-07 17:05 UTC (permalink / raw) To: Jacob Champion <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Christoph Berg <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]> Hi, On 2025-04-07 09:41:25 -0700, Jacob Champion wrote: > On Mon, Apr 7, 2025 at 7:21 AM Andres Freund <[email protected]> wrote: > > On 2025-04-04 17:27:46 -0700, Jacob Champion wrote: > > I think otherwise > > we run into the danger of the wrong library version being loaded in some > > cases. Imagine a program being told with libpq to use via rpath. But then we > > load the oauth module via a dlopen without a specified path - it'll just > > search the global library locations. > > Ah, you mean if the RPATH'd build doesn't have a flow, but the > globally installed one (with a different ABI) does? Yeah, that would > be a problem. That and more: Even if the RPATH libpq does have oauth support, the libpq-oauth won't necessarily be at the front of the global library search path. So afaict you'll often get a different libpq-oauth. Except perhaps on macos, where all this stuff works differently again. But I managed to unload the required knowledge out of my brain and don't want to further think about that :) > > > +# src/interfaces/libpq-oauth/exports.txt > > > +pg_fe_run_oauth_flow 1 > > > +pg_fe_cleanup_oauth_flow 2 > > > +pg_g_threadlock 3 > > > > The pg_g_threadlock thing seems pretty ugly. Can't we just pass that to the > > relevant functions? > > We can do it however we want, honestly, especially since the ABI isn't > public/stable. I chose this way just to ease review. I found it rather confusing that libpq looks up a symbol and then sets libpq-oauth's symbol to a pointer in libpq's namespace. > > But more fundamentally, are we actually using > > pg_g_threadlock anywhere? I removed all the releant code and the tests still > > seem to pass? > > If you have an older Curl installation, it is used. Newer libcurls > don't need it. Oh, wow. We hide the references to pg_g_threadlock behind a friggin macro? That's just ... Not your fault, I know... > A future simplification could be to pull the use of the threadlock > back into libpq, and have it perform a one-time > dlopen-plus-Curl-initialization under the lock... That would also get > rid of the dlerror() thread safety problems. But that's an awful lot > of moving parts under a mutex, which makes me a little nervous. I still think we should simply reject at configure time if curl init isn't threadsafe ;) > On Mon, Apr 7, 2025 at 7:43 AM Andres Freund <[email protected]> wrote: > > > Yes, this is correct. We want a shared module, not a shared library, in > > > meson parlance. > > > > It's not entirely obvious to me that we do want that. > > > > There recently was a breakage of building with PG on macos with meson, due to > > the meson folks implementing a feature request to move away from using > > bundles, as > > 1) bundles apparently aren't supported on iOS > > 2) there apparently aren't any restrictions left that require using bundles, > > and there haven't been for a while. > > Could you explain how this is related to .app bundles? I thought I was > just building a standard dylib. The other kind of bundles (what on earth apple was thinking with the naming here I don't know). Stuff liked with ld -bundle. > > > I don't know whether we need an exports file for libpq-oauth. The other > > > shared modules don't provide an export file, and I'm not sure whether this > > > is even supported for shared modules. I guess it doesn't hurt? > > > > It does seem just using PGDLLEXPORT would suffice here. > > My motivation was to strictly identify the ABI that we intend libpq to > use, to try to future-proof things for everybody. Especially since > we're duplicating functions from libpq that we'd rather not be. (The > use of RTLD_LOCAL maybe makes that more of a belt-and-suspenders > thing.) PGDLLEXPORT serves that purpose too, fwiw. These days we use compiler flags that restrict function visibility of everything not annotated with PGDLLEXPORT. However - that's all in c.h, port/win32.h,port/cygwin.h, , which libpq headers might not want to include. > Are there any downsides to the exports file? I think it's fine either way. Greetings, Andres Freund ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-07 17:32 Jacob Champion <[email protected]> parent: Andres Freund <[email protected]> 0 siblings, 0 replies; 36+ messages in thread From: Jacob Champion @ 2025-04-07 17:32 UTC (permalink / raw) To: Andres Freund <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Christoph Berg <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]> On Mon, Apr 7, 2025 at 10:05 AM Andres Freund <[email protected]> wrote: > On 2025-04-07 09:41:25 -0700, Jacob Champion wrote: > > Ah, you mean if the RPATH'd build doesn't have a flow, but the > > globally installed one (with a different ABI) does? Yeah, that would > > be a problem. > > That and more: Even if the RPATH libpq does have oauth support, the > libpq-oauth won't necessarily be at the front of the global library search > path. So afaict you'll often get a different libpq-oauth. ldopen() should respect RPATH, though? Either way, I agree with pushing the minor version into the name (or else deciding that we will keep the ABI completely stable across minor version bumps; not sure I want to guarantee that just yet). > > We can do it however we want, honestly, especially since the ABI isn't > > public/stable. I chose this way just to ease review. > > I found it rather confusing that libpq looks up a symbol and then sets > libpq-oauth's symbol to a pointer in libpq's namespace. Yeah, I think a one-time init call would make this nicer. > > A future simplification could be to pull the use of the threadlock > > back into libpq, and have it perform a one-time > > dlopen-plus-Curl-initialization under the lock... That would also get > > rid of the dlerror() thread safety problems. But that's an awful lot > > of moving parts under a mutex, which makes me a little nervous. > > I still think we should simply reject at configure time if curl init isn't > threadsafe ;) Practically speaking, I don't think that's a choice we can make. For example, RHEL won't have threadsafe Curl until 10. > > Could you explain how this is related to .app bundles? I thought I was > > just building a standard dylib. > > The other kind of bundles (what on earth apple was thinking with the naming > here I don't know). Stuff liked with ld -bundle. Ah, some new corner-case magic to learn... > These days we use compiler flags > that restrict function visibility of everything not annotated with > PGDLLEXPORT. Hm, I missed/forgot that. That is nice. Personally I like having a single file document the exports, so I'll keep it that way for now unless there are objections, but it's good to know it's not necessary. Thanks, --Jacob ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-07 18:47 Christoph Berg <[email protected]> parent: Jacob Champion <[email protected]> 2 siblings, 1 reply; 36+ messages in thread From: Christoph Berg @ 2025-04-07 18:47 UTC (permalink / raw) To: Jacob Champion <[email protected]>; +Cc: Andres Freund <[email protected]>; Peter Eisentraut <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]> Re: Jacob Champion > > > +This module ABI is an internal implementation detail, so it's subject to change > > > +without warning, even during minor releases (however unlikely). The compiled > > > +version of libpq-oauth should always match the compiled version of libpq. > > > > Shouldn't we then include the *minor* version in the soname? > > No objection here. Mmmmh. Since we are currently only talking about 3 symbols, it doesn't sound very likely that we'd have to bump this in a major branch. Putting the minor version into the filename would make looking at package diffs harder when upgrading. Do we really need this as opposed to some hardcoded number like libpq.so.5.18 ? Perhaps reusing the number from libpq.so.5.18 also for this lib would be the way to go? > > Which actually makes me wonder if we ought to instead load the library from a > > specific location... > > We could hardcode the disk location for version 1, I guess. I kind of > liked giving packagers the flexibility they're used to having from the > ld.so architecture, though. See below. pkglibdir or a subdirectory of it might make sense. Though for Debian, I'd actually prefer /usr/lib/$DEB_HOST_MULTIARCH/libpq/libpq-oauth... since the libpq packaging is independent from the major version packaging. > Are there technical downsides of putting it into $libdir? I understand > there are "people" downsides, since we don't really want to signal > that this is a publicly linkable thing... but surely if you go through > the process of linking our library (which has no SONAME, includes the > major/minor version in its -l option, and has no pkgconfig) and > shoving a private pointer to a threadlock into it, you can keep all > the pieces when they break? The Debian Policy expectation is that everything in libdir is a proper library that could be linked to, and that random private stuff should be elsewhere. But if being able to use the default lib search path is an important argument, we could put it there. Christoph ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-07 21:49 Jacob Champion <[email protected]> parent: Christoph Berg <[email protected]> 0 siblings, 1 reply; 36+ messages in thread From: Jacob Champion @ 2025-04-07 21:49 UTC (permalink / raw) To: Christoph Berg <[email protected]>; Jacob Champion <[email protected]>; Andres Freund <[email protected]>; Peter Eisentraut <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]> On Mon, Apr 7, 2025 at 11:47 AM Christoph Berg <[email protected]> wrote: > Mmmmh. Since we are currently only talking about 3 symbols, it doesn't > sound very likely that we'd have to bump this in a major branch. The ABI extends to the pointers we're using, though. This module uses PGconn* internals and libpq-int.h. [1] > Putting the minor version into the filename would make looking at > package diffs harder when upgrading. Do we really need this as opposed > to some hardcoded number like libpq.so.5.18 ? > > Perhaps reusing the number from libpq.so.5.18 also for this lib would > be the way to go? That doesn't address Andres' concern, though; if multiple installations all use libpq.so.5.18, they still can't necessarily mix and match. In fact you can't mix and match across different settings of ENABLE_SSL/GSS/SSPI, either. So I guess that nudges me towards pkglibdir/<some subdirectory>, to avoid major pain for some unlucky end user. > Though for Debian, I'd actually prefer > /usr/lib/$DEB_HOST_MULTIARCH/libpq/libpq-oauth... > since the libpq packaging is independent from the major version > packaging. Not sure I understand this. Do you mean you'd patch our lookup for Debian, to find it there instead of pkglibdir? I don't think we can adopt that ourselves, for the same reasons as above; the two sides have to be in lockstep. > The Debian Policy expectation is that everything in libdir is a proper > library that could be linked to, and that random private stuff should > be elsewhere. But if being able to use the default lib search path is > an important argument, we could put it there. I was hoping the default lib search would make your life (and ours) easier. If it doesn't, I can lock it down. Thanks, --Jacob [1] Future work ideas in this area include allowing other people to compile their own loadable flow plugin, so that the utilities can use it. (Only Device Authorization can be used by psql et al, for 18.) At that point, developers will need a limited API to twiddle the connection handle, and our builtin flow(s?) could use the same API. But that's not work we can tackle for 18. ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-07 21:58 Christoph Berg <[email protected]> parent: Jacob Champion <[email protected]> 0 siblings, 1 reply; 36+ messages in thread From: Christoph Berg @ 2025-04-07 21:58 UTC (permalink / raw) To: Jacob Champion <[email protected]>; +Cc: Andres Freund <[email protected]>; Peter Eisentraut <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]> Re: Jacob Champion > > Putting the minor version into the filename would make looking at > > package diffs harder when upgrading. Do we really need this as opposed > > to some hardcoded number like libpq.so.5.18 ? > > > > Perhaps reusing the number from libpq.so.5.18 also for this lib would > > be the way to go? > > That doesn't address Andres' concern, though; if multiple > installations all use libpq.so.5.18, they still can't necessarily mix > and match. Well the whole world is linking against libpq5.so and not breaking either. Why is this module worse? (I guess the answer is internal data structures... but does it have to be worse?) > > Though for Debian, I'd actually prefer > > /usr/lib/$DEB_HOST_MULTIARCH/libpq/libpq-oauth... > > since the libpq packaging is independent from the major version > > packaging. > > Not sure I understand this. Do you mean you'd patch our lookup for > Debian, to find it there instead of pkglibdir? I don't think we can > adopt that ourselves, for the same reasons as above; the two sides > have to be in lockstep. Because pkglibdir would be something like /usr/lib/postgresql/17/lib, even when there is only one libpq5 package for all major server versions on Debian. So if you have postgresql-16 installed, you'd end up with /usr/lib/postgresql/16/{bin,lib} everything from PG 16 /usr/lib/x86_64-linux-gnu/libpq* libpq5 /usr/lib/postgresql/17/lib/libpq-oauth.so ... which is weird. > [1] Future work ideas in this area include allowing other people to > compile their own loadable flow plugin, so that the utilities can use > it. (Only Device Authorization can be used by psql et al, for 18.) At > that point, developers will need a limited API to twiddle the > connection handle, and our builtin flow(s?) could use the same API. > But that's not work we can tackle for 18. Perhaps keep things simple for PG18 and choose a simple filename and location. If future extensions need something more elaborate, we can still switch later. Christoph ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-07 22:26 Jacob Champion <[email protected]> parent: Christoph Berg <[email protected]> 0 siblings, 1 reply; 36+ messages in thread From: Jacob Champion @ 2025-04-07 22:26 UTC (permalink / raw) To: Christoph Berg <[email protected]>; Jacob Champion <[email protected]>; Andres Freund <[email protected]>; Peter Eisentraut <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]> On Mon, Apr 7, 2025 at 2:58 PM Christoph Berg <[email protected]> wrote: > Why is this module worse? (I guess the answer is internal data > structures... but does it have to be worse?) It doesn't have to be, in general, and the coupling surface is small enough (libpq_append_conn_error) that we have a relatively easy path toward decoupling it in the future. But for 18, I suspect no one will be happy with me if I try to turn that inside out right this instant. The goal was just to turn an internal implementation detail into a delay-loaded internal implementation detail. > Because pkglibdir would be something like /usr/lib/postgresql/17/lib, > even when there is only one libpq5 package for all major server > versions on Debian. So if you have postgresql-16 installed, you'd end > up with > > /usr/lib/postgresql/16/{bin,lib} everything from PG 16 > /usr/lib/x86_64-linux-gnu/libpq* libpq5 > /usr/lib/postgresql/17/lib/libpq-oauth.so > > ... which is weird. Weird, sure -- but it's correct, right? Because you have PG17's OAuth flow installed. If someone comes to the list with a flow bug in three years, and I ask them what version they have installed, and they tell me "PG16, and it's loading /usr/lib/aarch64-linux-gnu/libpq/libpq-oauth.so." That won't be incredibly helpful IMHO. > > [1] Future work ideas in this area include allowing other people to > > compile their own loadable flow plugin, so that the utilities can use > > it. (Only Device Authorization can be used by psql et al, for 18.) At > > that point, developers will need a limited API to twiddle the > > connection handle, and our builtin flow(s?) could use the same API. > > But that's not work we can tackle for 18. > > Perhaps keep things simple for PG18 and choose a simple filename and > location. If future extensions need something more elaborate, we can > still switch later. Sounds good. Any opinions from the gallery on what a "libpq plugin subdirectory" in pkglibdir should be called? ("client", "modules", "plugins"...?) Is there still a good reason to put any explicit versioning into the filename if we do that? Thanks, --Jacob ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-08 02:10 Jacob Champion <[email protected]> parent: Jacob Champion <[email protected]> 0 siblings, 0 replies; 36+ messages in thread From: Jacob Champion @ 2025-04-08 02:10 UTC (permalink / raw) To: Christoph Berg <[email protected]>; Jacob Champion <[email protected]>; Andres Freund <[email protected]>; Peter Eisentraut <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]> On Mon, Apr 7, 2025 at 3:26 PM Jacob Champion <[email protected]> wrote: > Sounds good. Any opinions from the gallery on what a "libpq plugin > subdirectory" in pkglibdir should be called? ("client", "modules", > "plugins"...?) Hm, one immediate consequence of hardcoding pkglibdir is that we can no longer rely on LD_LIBRARY_PATH for pre-installation testing. (Contrast with the server, which is able to relocate extension paths based on its executable location.) --Jacob ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-08 21:32 Jacob Champion <[email protected]> parent: Jacob Champion <[email protected]> 2 siblings, 1 reply; 36+ messages in thread From: Jacob Champion @ 2025-04-08 21:32 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Christoph Berg <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]> On Mon, Apr 7, 2025 at 9:41 AM Jacob Champion <[email protected]> wrote: > > Not sure, the code looks correct at first glance. However, you could > > also just keep the libpq-oauth strings in the libpq catalog. There > > isn't really a need to make a separate one, since the versions you end > > up installing are locked to each other. So you could for example in > > libpq's nls.mk just add > > > > ../libpq-oauth/oauth-curl.c > > > > etc. to the files. > > Oh, that's an interesting idea. Thanks, I'll give it a try. A consequence of this is that our copy of libpq_binddomain isn't using the same mutex as libpq's copy to protect the "libpq-18" message domain. We could discuss whether or not it matters, since we don't support Windows, but it doesn't feel architecturally sound to me. If we want to reuse the same domain, I think the module should be using libpq's libpq_gettext(). (Which we could do, again through the magic of dependency injection.) > > Maybe it would also make sense to make libpq-oauth a subdirectory of the > > libpq directory instead of a peer. > > Works for me. It does not, however, work for our $(recurse) setup in the makefiles -- a shared library depending on a parent directory's shared library leads to infinite recursion, with the current tools -- so I'll keep it at the current directory level for now. Thanks, --Jacob ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-09 02:57 Jacob Champion <[email protected]> parent: Jacob Champion <[email protected]> 0 siblings, 1 reply; 36+ messages in thread From: Jacob Champion @ 2025-04-09 02:57 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Christoph Berg <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]> On Tue, Apr 8, 2025 at 2:32 PM Jacob Champion <[email protected]> wrote: > I think the module should be using > libpq's libpq_gettext(). (Which we could do, again through the magic > of dependency injection.) To illustrate what I mean, v3 introduces an initialization function that names the three internal dependencies (libpq_gettext, pg_g_threadlock, and conn->errorMessage) explicitly. I decided not to attempt injecting the variadic libpq_append_conn_error function, and instead focus a level below it, since we must depend directly on libpq_gettext anyway. This is maybe overkill, if it's decided that the two halves must always come from the same build, but I think it should decouple the two sides enough that this is now a question of user experience rather than ABI correctness. Is it acceptable/desirable for a build, which has not been configured --with-libcurl, to still pick up a compatible OAuth implementation installed by the distro? If so, we can go with a "bare" dlopen(). If that's not okay, I think we will probably need to use pkglibdir or some derivative, and introduce a way for tests (and users?) to override that directory selection. Unless someone has a good idea on how we can split the difference. --Jacob Attachments: [application/octet-stream] v3-0001-WIP-split-Device-Authorization-flow-into-dlopen-d.patch (23.6K, ../../CAOYmi+=mGJfJUdz13Ty86-epEX4g9zRNVq0+i+KmHsodAJ2FdQ@mail.gmail.com/2-v3-0001-WIP-split-Device-Authorization-flow-into-dlopen-d.patch) download | inline diff: From 19e6887b43a3d907465b9e36816ef6ef235207a6 Mon Sep 17 00:00:00 2001 From: Jacob Champion <[email protected]> Date: Wed, 26 Mar 2025 10:55:28 -0700 Subject: [PATCH v3] WIP: split Device Authorization flow into dlopen'd module See notes on mailing list. Co-authored-by: Daniel Gustafsson <[email protected]> --- meson.build | 12 +- src/interfaces/Makefile | 12 ++ src/interfaces/libpq-oauth/Makefile | 55 +++++ src/interfaces/libpq-oauth/README | 30 +++ src/interfaces/libpq-oauth/exports.txt | 4 + src/interfaces/libpq-oauth/meson.build | 31 +++ .../oauth-curl.c} | 10 +- src/interfaces/libpq-oauth/oauth-curl.h | 24 +++ src/interfaces/libpq-oauth/oauth-utils.c | 201 ++++++++++++++++++ src/interfaces/libpq-oauth/oauth-utils.h | 35 +++ src/interfaces/libpq/Makefile | 4 - src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-auth-oauth.c | 94 +++++++- src/interfaces/libpq/fe-auth-oauth.h | 4 +- src/interfaces/libpq/meson.build | 4 - src/interfaces/libpq/nls.mk | 12 +- 16 files changed, 502 insertions(+), 31 deletions(-) create mode 100644 src/interfaces/libpq-oauth/Makefile create mode 100644 src/interfaces/libpq-oauth/README create mode 100644 src/interfaces/libpq-oauth/exports.txt create mode 100644 src/interfaces/libpq-oauth/meson.build rename src/interfaces/{libpq/fe-auth-oauth-curl.c => libpq-oauth/oauth-curl.c} (99%) create mode 100644 src/interfaces/libpq-oauth/oauth-curl.h create mode 100644 src/interfaces/libpq-oauth/oauth-utils.c create mode 100644 src/interfaces/libpq-oauth/oauth-utils.h diff --git a/meson.build b/meson.build index 27717ad8976..6f1a8ea55ef 100644 --- a/meson.build +++ b/meson.build @@ -107,6 +107,7 @@ os_deps = [] backend_both_deps = [] backend_deps = [] libpq_deps = [] +libpq_oauth_deps = [] pg_sysroot = '' @@ -3251,17 +3252,18 @@ libpq_deps += [ gssapi, ldap_r, - # XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults - # during gss_acquire_cred(). This is possibly related to Curl's Heimdal - # dependency on that platform? - libcurl, libintl, ssl, ] +libpq_oauth_deps += [ + libcurl, +] + subdir('src/interfaces/libpq') -# fe_utils depends on libpq +# fe_utils and libpq-oauth depends on libpq subdir('src/fe_utils') +subdir('src/interfaces/libpq-oauth') # for frontend binaries frontend_code = declare_dependency( diff --git a/src/interfaces/Makefile b/src/interfaces/Makefile index 7d56b29d28f..e6822caa206 100644 --- a/src/interfaces/Makefile +++ b/src/interfaces/Makefile @@ -14,7 +14,19 @@ include $(top_builddir)/src/Makefile.global SUBDIRS = libpq ecpg +ifeq ($(with_libcurl), yes) +SUBDIRS += libpq-oauth +else +ALWAYS_SUBDIRS += libpq-oauth +endif + $(recurse) +$(recurse_always) all-ecpg-recurse: all-libpq-recurse install-ecpg-recurse: install-libpq-recurse + +ifeq ($(with_libcurl), yes) +all-libpq-oauth-recurse: all-libpq-recurse +install-libpq-oauth-recurse: install-libpq-recurse +endif diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile new file mode 100644 index 00000000000..f44766dd549 --- /dev/null +++ b/src/interfaces/libpq-oauth/Makefile @@ -0,0 +1,55 @@ +#------------------------------------------------------------------------- +# +# Makefile for libpq-oauth +# +# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1994, Regents of the University of California +# +# src/interfaces/libpq-oauth/Makefile +# +#------------------------------------------------------------------------- + +subdir = src/interfaces/libpq-oauth +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +PGFILEDESC = "libpq-oauth - device authorization OAuth support" + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. +NAME = libpq-oauth-$(MAJORVERSION) + +override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(CPPFLAGS) + +OBJS = \ + $(WIN32RES) \ + oauth-curl.o \ + oauth-utils.o + +SHLIB_LINK_INTERNAL = $(libpq_pgport_shlib) +SHLIB_LINK = -lcurl $(filter -lintl, $(LIBS)) +SHLIB_PREREQS = submake-libpq +SHLIB_EXPORTS = exports.txt + +# Disable -bundle_loader on macOS. +BE_DLLLIBS = + +# Make dependencies on pg_config_paths.h visible in all builds. +oauth-curl.o: oauth-curl.c $(top_builddir)/src/port/pg_config_paths.h + +$(top_builddir)/src/port/pg_config_paths.h: + $(MAKE) -C $(top_builddir)/src/port pg_config_paths.h + +all: all-lib + +# Shared library stuff +include $(top_srcdir)/src/Makefile.shlib + +install: all installdirs install-lib + +installdirs: installdirs-lib + +uninstall: uninstall-lib + +clean distclean: clean-lib + rm -f $(OBJS) diff --git a/src/interfaces/libpq-oauth/README b/src/interfaces/libpq-oauth/README new file mode 100644 index 00000000000..ef746617c71 --- /dev/null +++ b/src/interfaces/libpq-oauth/README @@ -0,0 +1,30 @@ +libpq-oauth is an optional module implementing the Device Authorization flow for +OAuth clients (RFC 8628). It was originally developed as part of libpq core and +later split out as its own shared library in order to isolate its dependency on +libcurl. (End users who don't want the Curl dependency can simply choose not to +install this module.) + +If a connection string allows the use of OAuth, the server asks for it, and a +libpq client has not installed its own custom OAuth flow, libpq will attempt to +delay-load this module using dlopen() and the following ABI. Failure to load +results in a failed connection. + += Load-Time ABI = + +This module ABI is an internal implementation detail, so it's subject to change +without warning, even during minor releases (however unlikely). The compiled +version of libpq-oauth should always match the compiled version of libpq. + +- PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +- void pg_fe_cleanup_oauth_flow(PGconn *conn); + +pg_fe_run_oauth_flow and pg_fe_cleanup_oauth_flow are implementations of +conn->async_auth and conn->cleanup_async_auth, respectively. + +- void libpq_oauth_init(pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + +At the moment, pg_fe_run_oauth_flow() relies on libpq's pg_g_threadlock and +libpq_gettext(), which must be injected by libpq before the flow is run. It also +relies on libpq to expose conn->errorMessage, via an errmsg_impl. diff --git a/src/interfaces/libpq-oauth/exports.txt b/src/interfaces/libpq-oauth/exports.txt new file mode 100644 index 00000000000..6891a83dbf9 --- /dev/null +++ b/src/interfaces/libpq-oauth/exports.txt @@ -0,0 +1,4 @@ +# src/interfaces/libpq-oauth/exports.txt +libpq_oauth_init 1 +pg_fe_run_oauth_flow 2 +pg_fe_cleanup_oauth_flow 3 diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build new file mode 100644 index 00000000000..79916e7aa62 --- /dev/null +++ b/src/interfaces/libpq-oauth/meson.build @@ -0,0 +1,31 @@ +# Copyright (c) 2022-2025, PostgreSQL Global Development Group + +if not libcurl.found() or host_system == 'windows' + subdir_done() +endif + +libpq_oauth_sources = files( + 'oauth-curl.c', + 'oauth-utils.c', +) + +export_file = custom_target('libpq-oauth.exports', + kwargs: gen_export_kwargs, +) + +# port needs to be in include path due to pthread-win32.h +libpq_oauth_inc = include_directories('.', '../libpq', '../../port') + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. +libpq_oauth_name = 'libpq-oauth-@0@'.format(pg_version_major) + +libpq_oauth_so = shared_module(libpq_oauth_name, + libpq_oauth_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_pch: pch_postgres_fe_h, + dependencies: [frontend_shlib_code, libpq, libpq_oauth_deps], + link_depends: export_file, + link_args: export_fmt.format(export_file.full_path()), + kwargs: default_lib_args, +) diff --git a/src/interfaces/libpq/fe-auth-oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c similarity index 99% rename from src/interfaces/libpq/fe-auth-oauth-curl.c rename to src/interfaces/libpq-oauth/oauth-curl.c index cd9c0323bb6..759cd494aae 100644 --- a/src/interfaces/libpq/fe-auth-oauth-curl.c +++ b/src/interfaces/libpq-oauth/oauth-curl.c @@ -1,6 +1,6 @@ /*------------------------------------------------------------------------- * - * fe-auth-oauth-curl.c + * oauth-curl.c * The libcurl implementation of OAuth/OIDC authentication, using the * OAuth Device Authorization Grant (RFC 8628). * @@ -8,7 +8,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * src/interfaces/libpq/fe-auth-oauth-curl.c + * src/interfaces/libpq-oauth/oauth-curl.c * *------------------------------------------------------------------------- */ @@ -29,8 +29,9 @@ #include "common/jsonapi.h" #include "fe-auth.h" #include "fe-auth-oauth.h" -#include "libpq-int.h" #include "mb/pg_wchar.h" +#include "oauth-curl.h" +#include "oauth-utils.h" /* * It's generally prudent to set a maximum response size to buffer in memory, @@ -2487,8 +2488,9 @@ prompt_user(struct async_ctx *actx, PGconn *conn) .verification_uri_complete = actx->authz.verification_uri_complete, .expires_in = actx->authz.expires_in, }; + PQauthDataHook_type hook = PQgetAuthDataHook(); - res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); + res = hook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); if (!res) { diff --git a/src/interfaces/libpq-oauth/oauth-curl.h b/src/interfaces/libpq-oauth/oauth-curl.h new file mode 100644 index 00000000000..248d0424ad0 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-curl.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * oauth-curl.h + * + * Definitions for OAuth Device Authorization module + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-curl.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_CURL_H +#define OAUTH_CURL_H + +#include "libpq-fe.h" + +/* Exported async-auth callbacks. */ +extern PGDLLEXPORT PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +extern PGDLLEXPORT void pg_fe_cleanup_oauth_flow(PGconn *conn); + +#endif /* OAUTH_CURL_H */ diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c new file mode 100644 index 00000000000..7a0949c071b --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.c @@ -0,0 +1,201 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.c + * + * "Glue" helpers providing a copy of some internal APIs from libpq. At + * some point in the future, we might be able to deduplicate. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq-oauth/oauth-utils.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include <signal.h> + +#include "libpq-int.h" +#include "oauth-utils.h" + +pgthreadlock_t pg_g_threadlock; +libpq_gettext_func libpq_gettext_impl; +conn_errorMessage_func conn_errorMessage; + +/*- + * Initializes libpq-oauth by setting necessary callbacks. + * + * The current implementation relies on the following private implementation + * details of libpq: + * + * - pg_g_threadlock: protects libcurl initialization if the underlying Curl + * installation is not threadsafe + * + * - libpq_gettext: translates error messages using libpq's message domain + * + * - conn->errorMessage: holds translated errors for the connection. This is + * handled through a translation shim, which avoids either depending on the + * offset of the errorMessage in PGconn, or needing to export the variadic + * libpq_append_conn_error(). + */ +void +libpq_oauth_init(pgthreadlock_t threadlock_impl, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl) +{ + pg_g_threadlock = threadlock_impl; + libpq_gettext_impl = gettext_impl; + conn_errorMessage = errmsg_impl; +} + +/* + * Append a formatted string to the error message buffer of the given + * connection, after translating it. This is a copy of libpq's internal API. + */ +void +libpq_append_conn_error(PGconn *conn, const char *fmt,...) +{ + int save_errno = errno; + bool done; + va_list args; + PQExpBuffer errorMessage = conn_errorMessage(conn); + + Assert(fmt[strlen(fmt) - 1] != '\n'); + + if (PQExpBufferBroken(errorMessage)) + return; /* already failed */ + + /* Loop in case we have to retry after enlarging the buffer. */ + do + { + errno = save_errno; + va_start(args, fmt); + done = appendPQExpBufferVA(errorMessage, libpq_gettext(fmt), args); + va_end(args); + } while (!done); + + appendPQExpBufferChar(errorMessage, '\n'); +} + +#ifdef ENABLE_NLS + +/* + * A shim that defers to the actual libpq_gettext(). + */ +char * +libpq_gettext(const char *msgid) +{ + if (!libpq_gettext_impl) + { + /* + * Possible if the libpq build doesn't enable NLS. That's a concerning + * mismatch, but in this particular case we can handle it. Try to warn + * a developer with an assertion, though. + */ + Assert(false); + + /* + * Note that callers of libpq_gettext() have to treat the return value + * as if it were const, because builds without NLS simply pass through + * their argument. + */ + return unconstify(char *, msgid); + } + + return libpq_gettext_impl(msgid); +} + +#endif /* ENABLE_NLS */ + +/* + * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment. + */ +bool +oauth_unsafe_debugging_enabled(void) +{ + const char *env = getenv("PGOAUTHDEBUG"); + + return (env && strcmp(env, "UNSAFE") == 0); +} + +/* + * Duplicate SOCK_ERRNO* definitions from libpq-int.h, for use by + * pq_block/reset_sigpipe(). + */ +#ifdef WIN32 +#define SOCK_ERRNO (WSAGetLastError()) +#define SOCK_ERRNO_SET(e) WSASetLastError(e) +#else +#define SOCK_ERRNO errno +#define SOCK_ERRNO_SET(e) (errno = (e)) +#endif + +/* + * Block SIGPIPE for this thread. This is a copy of libpq's internal API. + */ +int +pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending) +{ + sigset_t sigpipe_sigset; + sigset_t sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + /* Block SIGPIPE and save previous mask for later reset */ + SOCK_ERRNO_SET(pthread_sigmask(SIG_BLOCK, &sigpipe_sigset, osigset)); + if (SOCK_ERRNO) + return -1; + + /* We can have a pending SIGPIPE only if it was blocked before */ + if (sigismember(osigset, SIGPIPE)) + { + /* Is there a pending SIGPIPE? */ + if (sigpending(&sigset) != 0) + return -1; + + if (sigismember(&sigset, SIGPIPE)) + *sigpipe_pending = true; + else + *sigpipe_pending = false; + } + else + *sigpipe_pending = false; + + return 0; +} + +/* + * Discard any pending SIGPIPE and reset the signal mask. This is a copy of + * libpq's internal API. + */ +void +pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe) +{ + int save_errno = SOCK_ERRNO; + int signo; + sigset_t sigset; + + /* Clear SIGPIPE only if none was pending */ + if (got_epipe && !sigpipe_pending) + { + if (sigpending(&sigset) == 0 && + sigismember(&sigset, SIGPIPE)) + { + sigset_t sigpipe_sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + sigwait(&sigpipe_sigset, &signo); + } + } + + /* Restore saved block mask */ + pthread_sigmask(SIG_SETMASK, osigset, NULL); + + SOCK_ERRNO_SET(save_errno); +} diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h new file mode 100644 index 00000000000..279fc113248 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.h @@ -0,0 +1,35 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.h + * + * Definitions providing missing libpq internal APIs + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-utils.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_UTILS_H +#define OAUTH_UTILS_H + +#include "libpq-fe.h" +#include "pqexpbuffer.h" + +typedef char *(*libpq_gettext_func) (const char *msgid); +typedef PQExpBuffer (*conn_errorMessage_func) (PGconn *conn); + +/* Initializes libpq-oauth. */ +extern PGDLLEXPORT void libpq_oauth_init(pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + +/* Duplicated APIs, copied from libpq. */ +extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3); +extern bool oauth_unsafe_debugging_enabled(void); +extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending); +extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe); + +#endif /* OAUTH_UTILS_H */ diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 90b0b65db6f..8cf8d9e54d8 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -64,10 +64,6 @@ OBJS += \ fe-secure-gssapi.o endif -ifeq ($(with_libcurl),yes) -OBJS += fe-auth-oauth-curl.o -endif - ifeq ($(PORTNAME), cygwin) override shlib = cyg$(NAME)$(DLSUFFIX) endif diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index d5143766858..0625cf39e9a 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -210,3 +210,4 @@ PQsetAuthDataHook 207 PQgetAuthDataHook 208 PQdefaultAuthDataHook 209 PQfullProtocolVersion 210 +appendPQExpBufferVA 211 diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c index cf1a25e2ccc..416964ac335 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" +#ifndef WIN32 +#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,85 @@ cleanup_user_oauth_flow(PGconn *conn) state->async_ctx = NULL; } +typedef char *(*libpq_gettext_func) (const char *msgid); +typedef PQExpBuffer (*conn_errorMessage_func) (PGconn *conn); + +/* + * This shim is injected into libpq-oauth so that it doesn't depend on the + * offset of conn->errorMessage. + * + * TODO: look into exporting libpq_append_conn_error or a comparable API from + * libpq, instead. + */ +static PQExpBuffer +conn_errorMessage(PGconn *conn) +{ + return &conn->errorMessage; +} + +static bool +use_builtin_flow(PGconn *conn, fe_oauth_state *state) +{ +#ifdef WIN32 + return false; +#else + void (*init) (pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + PostgresPollingStatusType (*flow) (PGconn *conn); + void (*cleanup) (PGconn *conn); + + state->builtin_flow = dlopen("libpq-oauth-" PG_MAJORVERSION DLSUFFIX, + 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; + } + + /* + * Inject necessary function pointers into the module. + */ + init(pg_g_threadlock, +#ifdef ENABLE_NLS + libpq_gettext, +#else + NULL, +#endif + conn_errorMessage); + + /* Set our asynchronous callbacks. */ + conn->async_auth = flow; + conn->cleanup_async_auth = cleanup; + + return true; +#endif /* !WIN32 */ +} + /* * Chooses an OAuth client flow for the connection, which will retrieve a Bearer * token for presentation to the server. @@ -792,18 +876,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"); goto fail; - -#endif } return true; diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h index 3f1a7503a01..699ba42acc2 100644 --- a/src/interfaces/libpq/fe-auth-oauth.h +++ b/src/interfaces/libpq/fe-auth-oauth.h @@ -33,10 +33,10 @@ typedef struct PGconn *conn; void *async_ctx; + + void *builtin_flow; } fe_oauth_state; -extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); -extern void pg_fe_cleanup_oauth_flow(PGconn *conn); extern void pqClearOAuthToken(PGconn *conn); extern bool oauth_unsafe_debugging_enabled(void); diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build index 292fecf3320..47d38e9378f 100644 --- a/src/interfaces/libpq/meson.build +++ b/src/interfaces/libpq/meson.build @@ -38,10 +38,6 @@ if gssapi.found() ) endif -if libcurl.found() - libpq_sources += files('fe-auth-oauth-curl.c') -endif - export_file = custom_target('libpq.exports', kwargs: gen_export_kwargs, ) diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk index ae761265852..b87df277d93 100644 --- a/src/interfaces/libpq/nls.mk +++ b/src/interfaces/libpq/nls.mk @@ -13,15 +13,21 @@ GETTEXT_FILES = fe-auth.c \ fe-secure-common.c \ fe-secure-gssapi.c \ fe-secure-openssl.c \ - win32.c -GETTEXT_TRIGGERS = libpq_append_conn_error:2 \ + win32.c \ + ../libpq-oauth/oauth-curl.c \ + ../libpq-oauth/oauth-utils.c +GETTEXT_TRIGGERS = actx_error:2 \ + libpq_append_conn_error:2 \ libpq_append_error:2 \ libpq_gettext \ libpq_ngettext:1,2 \ + oauth_parse_set_error:2 \ pqInternalNotice:2 -GETTEXT_FLAGS = libpq_append_conn_error:2:c-format \ +GETTEXT_FLAGS = actx_error:2:c-format \ + libpq_append_conn_error:2:c-format \ libpq_append_error:2:c-format \ libpq_gettext:1:pass-c-format \ libpq_ngettext:1:pass-c-format \ libpq_ngettext:2:pass-c-format \ + oauth_parse_set_error:2:c-format \ pqInternalNotice:2:c-format -- 2.34.1 ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-09 08:14 Christoph Berg <[email protected]> parent: Jacob Champion <[email protected]> 0 siblings, 1 reply; 36+ messages in thread From: Christoph Berg @ 2025-04-09 08:14 UTC (permalink / raw) To: Jacob Champion <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]> Re: Jacob Champion > Is it acceptable/desirable for a build, which has not been configured > --with-libcurl, to still pick up a compatible OAuth implementation > installed by the distro? If so, we can go with a "bare" dlopen(). If > that's not okay, I think we will probably need to use pkglibdir or > some derivative, and introduce a way for tests (and users?) to > override that directory selection. Unless someone has a good idea on > how we can split the difference. One design goal could be reproducible builds-alike, that is, have libpq configured with or without libcurl be completely identical, and the feature being present is simply the libpq-oauth.so file existing or not. That might make using plain dlopen() more attractive. Christoph ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-09 12:50 Jacob Champion <[email protected]> parent: Christoph Berg <[email protected]> 0 siblings, 1 reply; 36+ messages in thread From: Jacob Champion @ 2025-04-09 12:50 UTC (permalink / raw) To: Christoph Berg <[email protected]>; Peter Eisentraut <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]>; Jelte Fennema-Nio <[email protected]> On Wed, Apr 9, 2025 at 1:14 AM Christoph Berg <[email protected]> wrote: > One design goal could be reproducible builds-alike, that is, have > libpq configured with or without libcurl be completely identical, and > the feature being present is simply the libpq-oauth.so file existing > or not. That might make using plain dlopen() more attractive. I think that's more or less what the current v3 does, but Jelte's point (which is upthread for me but downthread for others :D) is that making them identical might not actually be a desirable thing in this situation, because if you don't compile --with-libcurl, when you test that the feature is disabled you might potentially find that it is not. On Wed, Apr 9, 2025 at 1:39 AM Jelte Fennema-Nio <[email protected]> wrote: > How about ifdef-ing away the dlopen call if --with-libcurl is not specified. This sounds like a pretty decent, simple way to go. Christoph, does that ring any alarm bells from your perspective? Thanks, --Jacob ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-09 17:44 Christoph Berg <[email protected]> parent: Jacob Champion <[email protected]> 0 siblings, 1 reply; 36+ messages in thread From: Christoph Berg @ 2025-04-09 17:44 UTC (permalink / raw) To: Jacob Champion <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]>; Jelte Fennema-Nio <[email protected]> Re: Jacob Champion > > How about ifdef-ing away the dlopen call if --with-libcurl is not specified. > > This sounds like a pretty decent, simple way to go. Christoph, does > that ring any alarm bells from your perspective? Ok for me. The opposite that I said in the other mail was just a suggestion. Christoph ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-09 23:08 Jacob Champion <[email protected]> parent: Christoph Berg <[email protected]> 0 siblings, 2 replies; 36+ messages in thread From: Jacob Champion @ 2025-04-09 23:08 UTC (permalink / raw) To: Christoph Berg <[email protected]>; Jacob Champion <[email protected]>; Peter Eisentraut <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]>; Jelte Fennema-Nio <[email protected]> On Wed, Apr 9, 2025 at 10:44 AM Christoph Berg <[email protected]> wrote: > > Re: Jacob Champion > > > How about ifdef-ing away the dlopen call if --with-libcurl is not specified. > > > > This sounds like a pretty decent, simple way to go. Christoph, does > > that ring any alarm bells from your perspective? > > Ok for me. The opposite that I said in the other mail was just a > suggestion. Cool, thanks! v4 does it that way. It also errors out at configure time if you demand libpq-oauth on a platform that does not support it. The Autoconf side was still polluting LIBS and CPPFLAGS with Curl stuff. I have isolated them in v4, with some additional m4 boilerplate. IMO this makes the subtle difference between USE_LIBCURL (which means the libpq-oauth module is enabled to build) and HAVE_LIBCURL (which means you have libcurl installed locally) even more confusing. Christoph noted that this was also confusing from the packaging side, earlier, and Daniel proposed -Doauth-client/--with-oauth-client as the feature switch name instead. Any objections? Unfortunately it would mean a buildfarm email is in order, so we should get it locked in. Next up: staticlibs. Thanks, --Jacob Attachments: [application/octet-stream] v4-0001-WIP-split-Device-Authorization-flow-into-dlopen-d.patch (37.5K, ../../CAOYmi+n9DHS_xUatuuspdC8tjtaMzY8P11Y9y5Fz+2pjikkL9g@mail.gmail.com/2-v4-0001-WIP-split-Device-Authorization-flow-into-dlopen-d.patch) download | inline diff: From c500ad9f5653f02440449404ba1a975dab6ddfe6 Mon Sep 17 00:00:00 2001 From: Jacob Champion <[email protected]> Date: Wed, 26 Mar 2025 10:55:28 -0700 Subject: [PATCH v4] WIP: split Device Authorization flow into dlopen'd module See notes on mailing list. Co-authored-by: Daniel Gustafsson <[email protected]> --- config/programs.m4 | 17 +- configure | 50 ++++- configure.ac | 26 ++- meson.build | 22 +- src/Makefile.global.in | 3 + src/interfaces/Makefile | 12 ++ src/interfaces/libpq-oauth/Makefile | 55 +++++ src/interfaces/libpq-oauth/README | 30 +++ src/interfaces/libpq-oauth/exports.txt | 4 + src/interfaces/libpq-oauth/meson.build | 42 ++++ .../oauth-curl.c} | 60 +++--- src/interfaces/libpq-oauth/oauth-curl.h | 24 +++ src/interfaces/libpq-oauth/oauth-utils.c | 202 ++++++++++++++++++ src/interfaces/libpq-oauth/oauth-utils.h | 35 +++ src/interfaces/libpq/Makefile | 10 +- src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-auth-oauth.c | 102 ++++++++- src/interfaces/libpq/fe-auth-oauth.h | 4 +- src/interfaces/libpq/meson.build | 4 - src/interfaces/libpq/nls.mk | 12 +- src/makefiles/meson.build | 2 + 21 files changed, 635 insertions(+), 82 deletions(-) create mode 100644 src/interfaces/libpq-oauth/Makefile create mode 100644 src/interfaces/libpq-oauth/README create mode 100644 src/interfaces/libpq-oauth/exports.txt create mode 100644 src/interfaces/libpq-oauth/meson.build rename src/interfaces/{libpq/fe-auth-oauth-curl.c => libpq-oauth/oauth-curl.c} (98%) create mode 100644 src/interfaces/libpq-oauth/oauth-curl.h create mode 100644 src/interfaces/libpq-oauth/oauth-utils.c create mode 100644 src/interfaces/libpq-oauth/oauth-utils.h diff --git a/config/programs.m4 b/config/programs.m4 index 0a07feb37cc..0ad1e58b48d 100644 --- a/config/programs.m4 +++ b/config/programs.m4 @@ -286,9 +286,20 @@ AC_DEFUN([PGAC_CHECK_LIBCURL], [ AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file <curl/curl.h> is required for --with-libcurl])]) - AC_CHECK_LIB(curl, curl_multi_init, [], + AC_CHECK_LIB(curl, curl_multi_init, [ + AC_DEFINE([HAVE_LIBCURL], [1], [Define to 1 if you have the `curl' library (-lcurl).]) + AC_SUBST(LIBCURL_LDLIBS, -lcurl) + ], [AC_MSG_ERROR([library 'curl' does not provide curl_multi_init])]) + pgac_save_CPPFLAGS=$CPPFLAGS + pgac_save_LDFLAGS=$LDFLAGS + pgac_save_LIBS=$LIBS + + CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" + LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS" + LIBS="$LIBCURL_LDLIBS $LIBS" + # Check to see whether the current platform supports threadsafe Curl # initialization. AC_CACHE_CHECK([for curl_global_init thread safety], [pgac_cv__libcurl_threadsafe_init], @@ -338,4 +349,8 @@ AC_DEFUN([PGAC_CHECK_LIBCURL], *** lookups. Rebuild libcurl with the AsynchDNS feature enabled in order *** to use it with libpq.]) fi + + CPPFLAGS=$pgac_save_CPPFLAGS + LDFLAGS=$pgac_save_LDFLAGS + LIBS=$pgac_save_LIBS ])# PGAC_CHECK_LIBCURL diff --git a/configure b/configure index 8f4a5ab28ec..df1da549c4c 100755 --- a/configure +++ b/configure @@ -655,6 +655,7 @@ UUID_LIBS LDAP_LIBS_BE LDAP_LIBS_FE with_ssl +LIBCURL_LDLIBS PTHREAD_CFLAGS PTHREAD_LIBS PTHREAD_CC @@ -708,6 +709,8 @@ XML2_LIBS XML2_CFLAGS XML2_CONFIG with_libxml +LIBCURL_LDFLAGS +LIBCURL_CPPFLAGS LIBCURL_LIBS LIBCURL_CFLAGS with_libcurl @@ -9042,19 +9045,27 @@ $as_echo "yes" >&6; } fi - # We only care about -I, -D, and -L switches; - # note that -lcurl will be added by PGAC_CHECK_LIBCURL below. + # Curl's flags are kept separate from the standard CPPFLAGS/LDFLAGS. We use + # them only for libpq-oauth. + LIBCURL_CPPFLAGS= + LIBCURL_LDFLAGS= + + # We only care about -I, -D, and -L switches. Note that -lcurl will be added + # to LIBCURL_LDLIBS by PGAC_CHECK_LIBCURL, below. for pgac_option in $LIBCURL_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) LIBCURL_CPPFLAGS="$LIBCURL_CPPFLAGS $pgac_option";; esac done for pgac_option in $LIBCURL_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBCURL_LDFLAGS="$LIBCURL_LDFLAGS $pgac_option";; esac done + + + # OAuth requires python for testing if test "$with_python" != yes; then { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: *** OAuth support tests require --with-python to run" >&5 @@ -12517,9 +12528,6 @@ fi fi -# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults -# during gss_acquire_cred(). This is possibly related to Curl's Heimdal -# dependency on that platform? if test "$with_libcurl" = yes ; then ac_fn_c_check_header_mongrel "$LINENO" "curl/curl.h" "ac_cv_header_curl_curl_h" "$ac_includes_default" @@ -12567,17 +12575,26 @@ fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curl_curl_multi_init" >&5 $as_echo "$ac_cv_lib_curl_curl_multi_init" >&6; } if test "x$ac_cv_lib_curl_curl_multi_init" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LIBCURL 1 -_ACEOF - LIBS="-lcurl $LIBS" + +$as_echo "#define HAVE_LIBCURL 1" >>confdefs.h + + LIBCURL_LDLIBS=-lcurl + else as_fn_error $? "library 'curl' does not provide curl_multi_init" "$LINENO" 5 fi + pgac_save_CPPFLAGS=$CPPFLAGS + pgac_save_LDFLAGS=$LDFLAGS + pgac_save_LIBS=$LIBS + + CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" + LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS" + LIBS="$LIBCURL_LDLIBS $LIBS" + # Check to see whether the current platform supports threadsafe Curl # initialization. { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_global_init thread safety" >&5 @@ -12681,6 +12698,10 @@ $as_echo "$pgac_cv__libcurl_async_dns" >&6; } *** to use it with libpq." "$LINENO" 5 fi + CPPFLAGS=$pgac_save_CPPFLAGS + LDFLAGS=$pgac_save_LDFLAGS + LIBS=$pgac_save_LIBS + fi if test "$with_gssapi" = yes ; then @@ -14329,6 +14350,13 @@ done fi +if test "$with_libcurl" = yes ; then + # Error out early if this platform can't support libpq-oauth. + if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then + as_fn_error $? "client OAuth is not supported on this platform" "$LINENO" 5 + fi +fi + ## ## Types, structures, compiler characteristics ## diff --git a/configure.ac b/configure.ac index fc5f7475d07..218aeea1b3b 100644 --- a/configure.ac +++ b/configure.ac @@ -1033,19 +1033,27 @@ if test "$with_libcurl" = yes ; then # to explicitly set TLS 1.3 ciphersuites). PKG_CHECK_MODULES(LIBCURL, [libcurl >= 7.61.0]) - # We only care about -I, -D, and -L switches; - # note that -lcurl will be added by PGAC_CHECK_LIBCURL below. + # Curl's flags are kept separate from the standard CPPFLAGS/LDFLAGS. We use + # them only for libpq-oauth. + LIBCURL_CPPFLAGS= + LIBCURL_LDFLAGS= + + # We only care about -I, -D, and -L switches. Note that -lcurl will be added + # to LIBCURL_LDLIBS by PGAC_CHECK_LIBCURL, below. for pgac_option in $LIBCURL_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) LIBCURL_CPPFLAGS="$LIBCURL_CPPFLAGS $pgac_option";; esac done for pgac_option in $LIBCURL_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBCURL_LDFLAGS="$LIBCURL_LDFLAGS $pgac_option";; esac done + AC_SUBST(LIBCURL_CPPFLAGS) + AC_SUBST(LIBCURL_LDFLAGS) + # OAuth requires python for testing if test "$with_python" != yes; then AC_MSG_WARN([*** OAuth support tests require --with-python to run]) @@ -1340,9 +1348,6 @@ failure. It is possible the compiler isn't looking in the proper directory. Use --without-zlib to disable zlib support.])]) fi -# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults -# during gss_acquire_cred(). This is possibly related to Curl's Heimdal -# dependency on that platform? if test "$with_libcurl" = yes ; then PGAC_CHECK_LIBCURL fi @@ -1640,6 +1645,13 @@ if test "$PORTNAME" = "win32" ; then AC_CHECK_HEADERS(crtdefs.h) fi +if test "$with_libcurl" = yes ; then + # Error out early if this platform can't support libpq-oauth. + if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then + AC_MSG_ERROR([client OAuth is not supported on this platform]) + fi +fi + ## ## Types, structures, compiler characteristics ## diff --git a/meson.build b/meson.build index 27717ad8976..0f0df9b1af4 100644 --- a/meson.build +++ b/meson.build @@ -107,6 +107,7 @@ os_deps = [] backend_both_deps = [] backend_deps = [] libpq_deps = [] +libpq_oauth_deps = [] pg_sysroot = '' @@ -2587,6 +2588,7 @@ header_checks = [ 'xlocale.h', ] +header_macros = {} foreach header : header_checks varname = 'HAVE_' + header.underscorify().to_upper() @@ -2595,6 +2597,15 @@ foreach header : header_checks include_directories: postgres_inc, args: test_c_args) cdata.set(varname, found ? 1 : false, description: 'Define to 1 if you have the <@0@> header file.'.format(header)) + + # Mixing 1/false in cdata means we can't perform equality checks using + # cdata.get(), though, so store our defined header macros for later lookup. + # + # https://github.com/mesonbuild/meson/issues/11581 + # + if found + header_macros += {varname: true} + endif endforeach @@ -3251,17 +3262,18 @@ libpq_deps += [ gssapi, ldap_r, - # XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults - # during gss_acquire_cred(). This is possibly related to Curl's Heimdal - # dependency on that platform? - libcurl, libintl, ssl, ] +libpq_oauth_deps += [ + libcurl, +] + subdir('src/interfaces/libpq') -# fe_utils depends on libpq +# fe_utils and libpq-oauth depends on libpq subdir('src/fe_utils') +subdir('src/interfaces/libpq-oauth') # for frontend binaries frontend_code = declare_dependency( diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 737b2dd1869..eb9b5de75b4 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -343,6 +343,9 @@ perl_embed_ldflags = @perl_embed_ldflags@ AWK = @AWK@ LN_S = @LN_S@ +LIBCURL_CPPFLAGS = @LIBCURL_CPPFLAGS@ +LIBCURL_LDFLAGS = @LIBCURL_LDFLAGS@ +LIBCURL_LDLIBS = @LIBCURL_LDLIBS@ MSGFMT = @MSGFMT@ MSGFMT_FLAGS = @MSGFMT_FLAGS@ MSGMERGE = @MSGMERGE@ diff --git a/src/interfaces/Makefile b/src/interfaces/Makefile index 7d56b29d28f..e6822caa206 100644 --- a/src/interfaces/Makefile +++ b/src/interfaces/Makefile @@ -14,7 +14,19 @@ include $(top_builddir)/src/Makefile.global SUBDIRS = libpq ecpg +ifeq ($(with_libcurl), yes) +SUBDIRS += libpq-oauth +else +ALWAYS_SUBDIRS += libpq-oauth +endif + $(recurse) +$(recurse_always) all-ecpg-recurse: all-libpq-recurse install-ecpg-recurse: install-libpq-recurse + +ifeq ($(with_libcurl), yes) +all-libpq-oauth-recurse: all-libpq-recurse +install-libpq-oauth-recurse: install-libpq-recurse +endif diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile new file mode 100644 index 00000000000..8ed5a6a39c3 --- /dev/null +++ b/src/interfaces/libpq-oauth/Makefile @@ -0,0 +1,55 @@ +#------------------------------------------------------------------------- +# +# Makefile for libpq-oauth +# +# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1994, Regents of the University of California +# +# src/interfaces/libpq-oauth/Makefile +# +#------------------------------------------------------------------------- + +subdir = src/interfaces/libpq-oauth +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +PGFILEDESC = "libpq-oauth - device authorization OAuth support" + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. +NAME = libpq-oauth-$(MAJORVERSION) + +override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(LIBCURL_CPPFLAGS) $(CPPFLAGS) + +OBJS = \ + $(WIN32RES) \ + oauth-curl.o \ + oauth-utils.o + +SHLIB_LINK_INTERNAL = $(libpq_pgport_shlib) +SHLIB_LINK = $(LIBCURL_LDFLAGS) $(LIBCURL_LDLIBS) +SHLIB_PREREQS = submake-libpq +SHLIB_EXPORTS = exports.txt + +# Disable -bundle_loader on macOS. +BE_DLLLIBS = + +# Make dependencies on pg_config_paths.h visible in all builds. +oauth-curl.o: oauth-curl.c $(top_builddir)/src/port/pg_config_paths.h + +$(top_builddir)/src/port/pg_config_paths.h: + $(MAKE) -C $(top_builddir)/src/port pg_config_paths.h + +all: all-lib + +# Shared library stuff +include $(top_srcdir)/src/Makefile.shlib + +install: all installdirs install-lib + +installdirs: installdirs-lib + +uninstall: uninstall-lib + +clean distclean: clean-lib + rm -f $(OBJS) diff --git a/src/interfaces/libpq-oauth/README b/src/interfaces/libpq-oauth/README new file mode 100644 index 00000000000..ef746617c71 --- /dev/null +++ b/src/interfaces/libpq-oauth/README @@ -0,0 +1,30 @@ +libpq-oauth is an optional module implementing the Device Authorization flow for +OAuth clients (RFC 8628). It was originally developed as part of libpq core and +later split out as its own shared library in order to isolate its dependency on +libcurl. (End users who don't want the Curl dependency can simply choose not to +install this module.) + +If a connection string allows the use of OAuth, the server asks for it, and a +libpq client has not installed its own custom OAuth flow, libpq will attempt to +delay-load this module using dlopen() and the following ABI. Failure to load +results in a failed connection. + += Load-Time ABI = + +This module ABI is an internal implementation detail, so it's subject to change +without warning, even during minor releases (however unlikely). The compiled +version of libpq-oauth should always match the compiled version of libpq. + +- PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +- void pg_fe_cleanup_oauth_flow(PGconn *conn); + +pg_fe_run_oauth_flow and pg_fe_cleanup_oauth_flow are implementations of +conn->async_auth and conn->cleanup_async_auth, respectively. + +- void libpq_oauth_init(pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + +At the moment, pg_fe_run_oauth_flow() relies on libpq's pg_g_threadlock and +libpq_gettext(), which must be injected by libpq before the flow is run. It also +relies on libpq to expose conn->errorMessage, via an errmsg_impl. diff --git a/src/interfaces/libpq-oauth/exports.txt b/src/interfaces/libpq-oauth/exports.txt new file mode 100644 index 00000000000..6891a83dbf9 --- /dev/null +++ b/src/interfaces/libpq-oauth/exports.txt @@ -0,0 +1,4 @@ +# src/interfaces/libpq-oauth/exports.txt +libpq_oauth_init 1 +pg_fe_run_oauth_flow 2 +pg_fe_cleanup_oauth_flow 3 diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build new file mode 100644 index 00000000000..bf181fd5b96 --- /dev/null +++ b/src/interfaces/libpq-oauth/meson.build @@ -0,0 +1,42 @@ +# Copyright (c) 2022-2025, PostgreSQL Global Development Group + +oauth_flow_supported = ( + libcurl.found() + and (header_macros.has_key('HAVE_SYS_EVENT_H') + or header_macros.has_key('HAVE_SYS_EPOLL_H')) +) + +if libcurlopt.disabled() + subdir_done() +elif not oauth_flow_supported + if libcurlopt.enabled() + error('client OAuth is not supported on this platform') + endif + subdir_done() +endif + +libpq_oauth_sources = files( + 'oauth-curl.c', + 'oauth-utils.c', +) + +export_file = custom_target('libpq-oauth.exports', + kwargs: gen_export_kwargs, +) + +# port needs to be in include path due to pthread-win32.h +libpq_oauth_inc = include_directories('.', '../libpq', '../../port') + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. +libpq_oauth_name = 'libpq-oauth-@0@'.format(pg_version_major) + +libpq_oauth_so = shared_module(libpq_oauth_name, + libpq_oauth_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_pch: pch_postgres_fe_h, + dependencies: [frontend_shlib_code, libpq, libpq_oauth_deps], + link_depends: export_file, + link_args: export_fmt.format(export_file.full_path()), + kwargs: default_lib_args, +) diff --git a/src/interfaces/libpq/fe-auth-oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c similarity index 98% rename from src/interfaces/libpq/fe-auth-oauth-curl.c rename to src/interfaces/libpq-oauth/oauth-curl.c index cd9c0323bb6..d52125415bc 100644 --- a/src/interfaces/libpq/fe-auth-oauth-curl.c +++ b/src/interfaces/libpq-oauth/oauth-curl.c @@ -1,6 +1,6 @@ /*------------------------------------------------------------------------- * - * fe-auth-oauth-curl.c + * oauth-curl.c * The libcurl implementation of OAuth/OIDC authentication, using the * OAuth Device Authorization Grant (RFC 8628). * @@ -8,7 +8,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * src/interfaces/libpq/fe-auth-oauth-curl.c + * src/interfaces/libpq-oauth/oauth-curl.c * *------------------------------------------------------------------------- */ @@ -17,20 +17,23 @@ #include <curl/curl.h> #include <math.h> -#ifdef HAVE_SYS_EPOLL_H +#include <unistd.h> + +#if defined(HAVE_SYS_EPOLL_H) #include <sys/epoll.h> #include <sys/timerfd.h> -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) #include <sys/event.h> +#else +#error libpq-oauth is not supported on this platform #endif -#include <unistd.h> #include "common/jsonapi.h" #include "fe-auth.h" #include "fe-auth-oauth.h" -#include "libpq-int.h" #include "mb/pg_wchar.h" +#include "oauth-curl.h" +#include "oauth-utils.h" /* * It's generally prudent to set a maximum response size to buffer in memory, @@ -1110,7 +1113,7 @@ parse_access_token(struct async_ctx *actx, struct token *tok) static bool setup_multiplexer(struct async_ctx *actx) { -#ifdef HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct epoll_event ev = {.events = EPOLLIN}; actx->mux = epoll_create1(EPOLL_CLOEXEC); @@ -1134,8 +1137,7 @@ setup_multiplexer(struct async_ctx *actx) } return true; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) actx->mux = kqueue(); if (actx->mux < 0) { @@ -1158,10 +1160,9 @@ setup_multiplexer(struct async_ctx *actx) } return true; +#else +#error setup_multiplexer is not implemented on this platform #endif - - actx_error(actx, "libpq does not support the Device Authorization flow on this platform"); - return false; } /* @@ -1174,7 +1175,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, { struct async_ctx *actx = ctx; -#ifdef HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct epoll_event ev = {0}; int res; int op = EPOLL_CTL_ADD; @@ -1230,8 +1231,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, } return 0; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) struct kevent ev[2] = {0}; struct kevent ev_out[2]; struct timespec timeout = {0}; @@ -1312,10 +1312,9 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, } return 0; +#else +#error register_socket is not implemented on this platform #endif - - actx_error(actx, "libpq does not support multiplexer sockets on this platform"); - return -1; } /* @@ -1334,7 +1333,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, static bool set_timer(struct async_ctx *actx, long timeout) { -#if HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct itimerspec spec = {0}; if (timeout < 0) @@ -1363,8 +1362,7 @@ set_timer(struct async_ctx *actx, long timeout) } return true; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) struct kevent ev; #ifdef __NetBSD__ @@ -1419,10 +1417,9 @@ set_timer(struct async_ctx *actx, long timeout) } return true; +#else +#error set_timer is not implemented on this platform #endif - - actx_error(actx, "libpq does not support timers on this platform"); - return false; } /* @@ -1433,7 +1430,7 @@ set_timer(struct async_ctx *actx, long timeout) static int timer_expired(struct async_ctx *actx) { -#if HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct itimerspec spec = {0}; if (timerfd_gettime(actx->timerfd, &spec) < 0) @@ -1453,8 +1450,7 @@ timer_expired(struct async_ctx *actx) /* If the remaining time to expiration is zero, we're done. */ return (spec.it_value.tv_sec == 0 && spec.it_value.tv_nsec == 0); -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) int res; /* Is the timer queue ready? */ @@ -1466,10 +1462,9 @@ timer_expired(struct async_ctx *actx) } return (res > 0); +#else +#error timer_expired is not implemented on this platform #endif - - actx_error(actx, "libpq does not support timers on this platform"); - return -1; } /* @@ -2487,8 +2482,9 @@ prompt_user(struct async_ctx *actx, PGconn *conn) .verification_uri_complete = actx->authz.verification_uri_complete, .expires_in = actx->authz.expires_in, }; + PQauthDataHook_type hook = PQgetAuthDataHook(); - res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); + res = hook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); if (!res) { diff --git a/src/interfaces/libpq-oauth/oauth-curl.h b/src/interfaces/libpq-oauth/oauth-curl.h new file mode 100644 index 00000000000..248d0424ad0 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-curl.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * oauth-curl.h + * + * Definitions for OAuth Device Authorization module + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-curl.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_CURL_H +#define OAUTH_CURL_H + +#include "libpq-fe.h" + +/* Exported async-auth callbacks. */ +extern PGDLLEXPORT PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +extern PGDLLEXPORT void pg_fe_cleanup_oauth_flow(PGconn *conn); + +#endif /* OAUTH_CURL_H */ diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c new file mode 100644 index 00000000000..2bdbf904743 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.c @@ -0,0 +1,202 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.c + * + * "Glue" helpers providing a copy of some internal APIs from libpq. At + * some point in the future, we might be able to deduplicate. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq-oauth/oauth-utils.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include <signal.h> + +#include "libpq-int.h" +#include "oauth-utils.h" + +static libpq_gettext_func libpq_gettext_impl; +static conn_errorMessage_func conn_errorMessage; + +pgthreadlock_t pg_g_threadlock; + +/*- + * Initializes libpq-oauth by setting necessary callbacks. + * + * The current implementation relies on the following private implementation + * details of libpq: + * + * - pg_g_threadlock: protects libcurl initialization if the underlying Curl + * installation is not threadsafe + * + * - libpq_gettext: translates error messages using libpq's message domain + * + * - conn->errorMessage: holds translated errors for the connection. This is + * handled through a translation shim, which avoids either depending on the + * offset of the errorMessage in PGconn, or needing to export the variadic + * libpq_append_conn_error(). + */ +void +libpq_oauth_init(pgthreadlock_t threadlock_impl, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl) +{ + pg_g_threadlock = threadlock_impl; + libpq_gettext_impl = gettext_impl; + conn_errorMessage = errmsg_impl; +} + +/* + * Append a formatted string to the error message buffer of the given + * connection, after translating it. This is a copy of libpq's internal API. + */ +void +libpq_append_conn_error(PGconn *conn, const char *fmt,...) +{ + int save_errno = errno; + bool done; + va_list args; + PQExpBuffer errorMessage = conn_errorMessage(conn); + + Assert(fmt[strlen(fmt) - 1] != '\n'); + + if (PQExpBufferBroken(errorMessage)) + return; /* already failed */ + + /* Loop in case we have to retry after enlarging the buffer. */ + do + { + errno = save_errno; + va_start(args, fmt); + done = appendPQExpBufferVA(errorMessage, libpq_gettext(fmt), args); + va_end(args); + } while (!done); + + appendPQExpBufferChar(errorMessage, '\n'); +} + +#ifdef ENABLE_NLS + +/* + * A shim that defers to the actual libpq_gettext(). + */ +char * +libpq_gettext(const char *msgid) +{ + if (!libpq_gettext_impl) + { + /* + * Possible if the libpq build doesn't enable NLS. That's a concerning + * mismatch, but in this particular case we can handle it. Try to warn + * a developer with an assertion, though. + */ + Assert(false); + + /* + * Note that callers of libpq_gettext() have to treat the return value + * as if it were const, because builds without NLS simply pass through + * their argument. + */ + return unconstify(char *, msgid); + } + + return libpq_gettext_impl(msgid); +} + +#endif /* ENABLE_NLS */ + +/* + * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment. + */ +bool +oauth_unsafe_debugging_enabled(void) +{ + const char *env = getenv("PGOAUTHDEBUG"); + + return (env && strcmp(env, "UNSAFE") == 0); +} + +/* + * Duplicate SOCK_ERRNO* definitions from libpq-int.h, for use by + * pq_block/reset_sigpipe(). + */ +#ifdef WIN32 +#define SOCK_ERRNO (WSAGetLastError()) +#define SOCK_ERRNO_SET(e) WSASetLastError(e) +#else +#define SOCK_ERRNO errno +#define SOCK_ERRNO_SET(e) (errno = (e)) +#endif + +/* + * Block SIGPIPE for this thread. This is a copy of libpq's internal API. + */ +int +pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending) +{ + sigset_t sigpipe_sigset; + sigset_t sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + /* Block SIGPIPE and save previous mask for later reset */ + SOCK_ERRNO_SET(pthread_sigmask(SIG_BLOCK, &sigpipe_sigset, osigset)); + if (SOCK_ERRNO) + return -1; + + /* We can have a pending SIGPIPE only if it was blocked before */ + if (sigismember(osigset, SIGPIPE)) + { + /* Is there a pending SIGPIPE? */ + if (sigpending(&sigset) != 0) + return -1; + + if (sigismember(&sigset, SIGPIPE)) + *sigpipe_pending = true; + else + *sigpipe_pending = false; + } + else + *sigpipe_pending = false; + + return 0; +} + +/* + * Discard any pending SIGPIPE and reset the signal mask. This is a copy of + * libpq's internal API. + */ +void +pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe) +{ + int save_errno = SOCK_ERRNO; + int signo; + sigset_t sigset; + + /* Clear SIGPIPE only if none was pending */ + if (got_epipe && !sigpipe_pending) + { + if (sigpending(&sigset) == 0 && + sigismember(&sigset, SIGPIPE)) + { + sigset_t sigpipe_sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + sigwait(&sigpipe_sigset, &signo); + } + } + + /* Restore saved block mask */ + pthread_sigmask(SIG_SETMASK, osigset, NULL); + + SOCK_ERRNO_SET(save_errno); +} diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h new file mode 100644 index 00000000000..279fc113248 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.h @@ -0,0 +1,35 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.h + * + * Definitions providing missing libpq internal APIs + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-utils.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_UTILS_H +#define OAUTH_UTILS_H + +#include "libpq-fe.h" +#include "pqexpbuffer.h" + +typedef char *(*libpq_gettext_func) (const char *msgid); +typedef PQExpBuffer (*conn_errorMessage_func) (PGconn *conn); + +/* Initializes libpq-oauth. */ +extern PGDLLEXPORT void libpq_oauth_init(pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + +/* Duplicated APIs, copied from libpq. */ +extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3); +extern bool oauth_unsafe_debugging_enabled(void); +extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending); +extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe); + +#endif /* OAUTH_UTILS_H */ diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 90b0b65db6f..b5346181b9a 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -64,10 +64,6 @@ OBJS += \ fe-secure-gssapi.o endif -ifeq ($(with_libcurl),yes) -OBJS += fe-auth-oauth-curl.o -endif - ifeq ($(PORTNAME), cygwin) override shlib = cyg$(NAME)$(DLSUFFIX) endif @@ -86,7 +82,7 @@ endif # that are built correctly for use in a shlib. SHLIB_LINK_INTERNAL = -lpgcommon_shlib -lpgport_shlib ifneq ($(PORTNAME), win32) -SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lcurl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) +SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) else SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi32 -lssl -lsocket -lnsl -lresolv -lintl -lm $(PTHREAD_LIBS), $(LIBS)) $(LDAP_LIBS_FE) endif @@ -115,8 +111,6 @@ backend_src = $(top_srcdir)/src/backend # which seems to insert references to that even in pure C code. Excluding # __tsan_func_exit is necessary when using ThreadSanitizer data race detector # which use this function for instrumentation of function exit. -# libcurl registers an exit handler in the memory debugging code when running -# with LeakSanitizer. # Skip the test when profiling, as gcc may insert exit() calls for that. # Also skip the test on platforms where libpq infrastructure may be provided # by statically-linked libraries, as we can't expect them to honor this @@ -124,7 +118,7 @@ backend_src = $(top_srcdir)/src/backend libpq-refs-stamp: $(shlib) ifneq ($(enable_coverage), yes) ifeq (,$(filter solaris,$(PORTNAME))) - @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit -e _atexit | grep exit; then \ + @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit | grep exit; then \ echo 'libpq must not be calling any function which invokes exit'; exit 1; \ fi endif diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index d5143766858..0625cf39e9a 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -210,3 +210,4 @@ PQsetAuthDataHook 207 PQgetAuthDataHook 208 PQdefaultAuthDataHook 209 PQfullProtocolVersion 210 +appendPQExpBufferVA 211 diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c index cf1a25e2ccc..5bea1e059a2 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" +#ifndef WIN32 +#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,93 @@ cleanup_user_oauth_flow(PGconn *conn) state->async_ctx = NULL; } +#ifdef USE_LIBCURL + +typedef char *(*libpq_gettext_func) (const char *msgid); +typedef PQExpBuffer (*conn_errorMessage_func) (PGconn *conn); + +/* + * This shim is injected into libpq-oauth so that it doesn't depend on the + * offset of conn->errorMessage. + * + * TODO: look into exporting libpq_append_conn_error or a comparable API from + * libpq, instead. + */ +static PQExpBuffer +conn_errorMessage(PGconn *conn) +{ + return &conn->errorMessage; +} + +static bool +use_builtin_flow(PGconn *conn, fe_oauth_state *state) +{ + void (*init) (pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + PostgresPollingStatusType (*flow) (PGconn *conn); + void (*cleanup) (PGconn *conn); + + state->builtin_flow = dlopen("libpq-oauth-" PG_MAJORVERSION DLSUFFIX, + 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; + } + + /* + * Inject necessary function pointers into the module. + */ + init(pg_g_threadlock, +#ifdef ENABLE_NLS + libpq_gettext, +#else + NULL, +#endif + conn_errorMessage); + + /* Set our asynchronous callbacks. */ + conn->async_auth = flow; + conn->cleanup_async_auth = cleanup; + + return true; +} + +#else /* !USE_LIBCURL */ + +static bool +use_builtin_flow(PGconn *conn, fe_oauth_state *state) +{ + return false; +} + +#endif /* USE_LIBCURL */ + /* * Chooses an OAuth client flow for the connection, which will retrieve a Bearer * token for presentation to the server. @@ -792,18 +884,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"); goto fail; - -#endif } return true; diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h index 3f1a7503a01..699ba42acc2 100644 --- a/src/interfaces/libpq/fe-auth-oauth.h +++ b/src/interfaces/libpq/fe-auth-oauth.h @@ -33,10 +33,10 @@ typedef struct PGconn *conn; void *async_ctx; + + void *builtin_flow; } fe_oauth_state; -extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); -extern void pg_fe_cleanup_oauth_flow(PGconn *conn); extern void pqClearOAuthToken(PGconn *conn); extern bool oauth_unsafe_debugging_enabled(void); diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build index 292fecf3320..47d38e9378f 100644 --- a/src/interfaces/libpq/meson.build +++ b/src/interfaces/libpq/meson.build @@ -38,10 +38,6 @@ if gssapi.found() ) endif -if libcurl.found() - libpq_sources += files('fe-auth-oauth-curl.c') -endif - export_file = custom_target('libpq.exports', kwargs: gen_export_kwargs, ) diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk index ae761265852..b87df277d93 100644 --- a/src/interfaces/libpq/nls.mk +++ b/src/interfaces/libpq/nls.mk @@ -13,15 +13,21 @@ GETTEXT_FILES = fe-auth.c \ fe-secure-common.c \ fe-secure-gssapi.c \ fe-secure-openssl.c \ - win32.c -GETTEXT_TRIGGERS = libpq_append_conn_error:2 \ + win32.c \ + ../libpq-oauth/oauth-curl.c \ + ../libpq-oauth/oauth-utils.c +GETTEXT_TRIGGERS = actx_error:2 \ + libpq_append_conn_error:2 \ libpq_append_error:2 \ libpq_gettext \ libpq_ngettext:1,2 \ + oauth_parse_set_error:2 \ pqInternalNotice:2 -GETTEXT_FLAGS = libpq_append_conn_error:2:c-format \ +GETTEXT_FLAGS = actx_error:2:c-format \ + libpq_append_conn_error:2:c-format \ libpq_append_error:2:c-format \ libpq_gettext:1:pass-c-format \ libpq_ngettext:1:pass-c-format \ libpq_ngettext:2:pass-c-format \ + oauth_parse_set_error:2:c-format \ pqInternalNotice:2:c-format diff --git a/src/makefiles/meson.build b/src/makefiles/meson.build index 46d8da070e8..f2ba5b38124 100644 --- a/src/makefiles/meson.build +++ b/src/makefiles/meson.build @@ -201,6 +201,8 @@ pgxs_empty = [ 'ICU_LIBS', 'LIBURING_CFLAGS', 'LIBURING_LIBS', + + 'LIBCURL_CPPFLAGS', 'LIBCURL_LDFLAGS', 'LIBCURL_LDLIBS', ] if host_system == 'windows' and cc.get_argument_syntax() != 'msvc' -- 2.34.1 ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-09 23:42 Jelte Fennema-Nio <[email protected]> parent: Jacob Champion <[email protected]> 1 sibling, 1 reply; 36+ messages in thread From: Jelte Fennema-Nio @ 2025-04-09 23:42 UTC (permalink / raw) To: Jacob Champion <[email protected]>; +Cc: Christoph Berg <[email protected]>; Peter Eisentraut <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]> On Thu, Apr 10, 2025, 07:08 Jacob Champion <[email protected]> wrote: > Christoph noted that this was also confusing from the packaging side, > earlier, and Daniel proposed -Doauth-client/--with-oauth-client as the > feature switch name instead. > +1 Next up: staticlibs. > I think your suggestion of not using any .so files would best there (from w user perspective). I'd be quite surprised if a static build still resulted in me having to manage shared library files anyway. > ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-11 00:12 Jacob Champion <[email protected]> parent: Jelte Fennema-Nio <[email protected]> 0 siblings, 0 replies; 36+ messages in thread From: Jacob Champion @ 2025-04-11 00:12 UTC (permalink / raw) To: Jelte Fennema-Nio <[email protected]>; +Cc: Christoph Berg <[email protected]>; Peter Eisentraut <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]> On Wed, Apr 9, 2025 at 4:42 PM Jelte Fennema-Nio <[email protected]> wrote: > I think your suggestion of not using any .so files would best there (from w user perspective). I'd be quite surprised if a static build still resulted in me having to manage shared library files anyway. Done this way in v5. I had planned to separate the implementations by a #define, but I ran into issues with Makefile.shlib, so I split the shared and dynamic versions into separate files. I just now realized that we do something about this exact problem in src/common, so I'll see if I can copy its technique for the next go round. In the next version, I'll try to add --with-oauth-client while keeping --with-libcurl as an alias, to let the buildfarm migrate off of it before it's removed. Thanks! --Jacob Attachments: [application/octet-stream] v5-0001-WIP-split-Device-Authorization-flow-into-dlopen-d.patch (43.1K, ../../CAOYmi+nQRtnvFCdjRx+rA=MS1XTB8JTzj0q2o+zitzw=DSCYWg@mail.gmail.com/2-v5-0001-WIP-split-Device-Authorization-flow-into-dlopen-d.patch) download | inline diff: From 1e500c077aef3dfd17c5423d9f63639a75c2fd80 Mon Sep 17 00:00:00 2001 From: Jacob Champion <[email protected]> Date: Wed, 26 Mar 2025 10:55:28 -0700 Subject: [PATCH v5] WIP: split Device Authorization flow into dlopen'd module See notes on mailing list. Co-authored-by: Daniel Gustafsson <[email protected]> --- config/programs.m4 | 17 +- configure | 50 ++++- configure.ac | 26 ++- meson.build | 22 +- src/Makefile.global.in | 3 + src/interfaces/Makefile | 12 ++ src/interfaces/libpq-oauth/Makefile | 65 ++++++ src/interfaces/libpq-oauth/README | 30 +++ src/interfaces/libpq-oauth/exports.txt | 4 + src/interfaces/libpq-oauth/meson.build | 54 +++++ .../oauth-curl.c} | 60 +++--- src/interfaces/libpq-oauth/oauth-curl.h | 24 +++ src/interfaces/libpq-oauth/oauth-utils.c | 202 ++++++++++++++++++ src/interfaces/libpq-oauth/oauth-utils.h | 35 +++ src/interfaces/libpq/Makefile | 20 +- src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-auth-oauth-dynamic.c | 109 ++++++++++ src/interfaces/libpq/fe-auth-oauth-static.c | 40 ++++ src/interfaces/libpq/fe-auth-oauth.c | 26 ++- src/interfaces/libpq/fe-auth-oauth.h | 5 +- src/interfaces/libpq/meson.build | 8 +- src/interfaces/libpq/nls.mk | 12 +- src/makefiles/meson.build | 2 + 23 files changed, 747 insertions(+), 80 deletions(-) create mode 100644 src/interfaces/libpq-oauth/Makefile create mode 100644 src/interfaces/libpq-oauth/README create mode 100644 src/interfaces/libpq-oauth/exports.txt create mode 100644 src/interfaces/libpq-oauth/meson.build rename src/interfaces/{libpq/fe-auth-oauth-curl.c => libpq-oauth/oauth-curl.c} (98%) create mode 100644 src/interfaces/libpq-oauth/oauth-curl.h create mode 100644 src/interfaces/libpq-oauth/oauth-utils.c create mode 100644 src/interfaces/libpq-oauth/oauth-utils.h create mode 100644 src/interfaces/libpq/fe-auth-oauth-dynamic.c create mode 100644 src/interfaces/libpq/fe-auth-oauth-static.c diff --git a/config/programs.m4 b/config/programs.m4 index 0a07feb37cc..0ad1e58b48d 100644 --- a/config/programs.m4 +++ b/config/programs.m4 @@ -286,9 +286,20 @@ AC_DEFUN([PGAC_CHECK_LIBCURL], [ AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file <curl/curl.h> is required for --with-libcurl])]) - AC_CHECK_LIB(curl, curl_multi_init, [], + AC_CHECK_LIB(curl, curl_multi_init, [ + AC_DEFINE([HAVE_LIBCURL], [1], [Define to 1 if you have the `curl' library (-lcurl).]) + AC_SUBST(LIBCURL_LDLIBS, -lcurl) + ], [AC_MSG_ERROR([library 'curl' does not provide curl_multi_init])]) + pgac_save_CPPFLAGS=$CPPFLAGS + pgac_save_LDFLAGS=$LDFLAGS + pgac_save_LIBS=$LIBS + + CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" + LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS" + LIBS="$LIBCURL_LDLIBS $LIBS" + # Check to see whether the current platform supports threadsafe Curl # initialization. AC_CACHE_CHECK([for curl_global_init thread safety], [pgac_cv__libcurl_threadsafe_init], @@ -338,4 +349,8 @@ AC_DEFUN([PGAC_CHECK_LIBCURL], *** lookups. Rebuild libcurl with the AsynchDNS feature enabled in order *** to use it with libpq.]) fi + + CPPFLAGS=$pgac_save_CPPFLAGS + LDFLAGS=$pgac_save_LDFLAGS + LIBS=$pgac_save_LIBS ])# PGAC_CHECK_LIBCURL diff --git a/configure b/configure index 8f4a5ab28ec..df1da549c4c 100755 --- a/configure +++ b/configure @@ -655,6 +655,7 @@ UUID_LIBS LDAP_LIBS_BE LDAP_LIBS_FE with_ssl +LIBCURL_LDLIBS PTHREAD_CFLAGS PTHREAD_LIBS PTHREAD_CC @@ -708,6 +709,8 @@ XML2_LIBS XML2_CFLAGS XML2_CONFIG with_libxml +LIBCURL_LDFLAGS +LIBCURL_CPPFLAGS LIBCURL_LIBS LIBCURL_CFLAGS with_libcurl @@ -9042,19 +9045,27 @@ $as_echo "yes" >&6; } fi - # We only care about -I, -D, and -L switches; - # note that -lcurl will be added by PGAC_CHECK_LIBCURL below. + # Curl's flags are kept separate from the standard CPPFLAGS/LDFLAGS. We use + # them only for libpq-oauth. + LIBCURL_CPPFLAGS= + LIBCURL_LDFLAGS= + + # We only care about -I, -D, and -L switches. Note that -lcurl will be added + # to LIBCURL_LDLIBS by PGAC_CHECK_LIBCURL, below. for pgac_option in $LIBCURL_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) LIBCURL_CPPFLAGS="$LIBCURL_CPPFLAGS $pgac_option";; esac done for pgac_option in $LIBCURL_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBCURL_LDFLAGS="$LIBCURL_LDFLAGS $pgac_option";; esac done + + + # OAuth requires python for testing if test "$with_python" != yes; then { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: *** OAuth support tests require --with-python to run" >&5 @@ -12517,9 +12528,6 @@ fi fi -# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults -# during gss_acquire_cred(). This is possibly related to Curl's Heimdal -# dependency on that platform? if test "$with_libcurl" = yes ; then ac_fn_c_check_header_mongrel "$LINENO" "curl/curl.h" "ac_cv_header_curl_curl_h" "$ac_includes_default" @@ -12567,17 +12575,26 @@ fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curl_curl_multi_init" >&5 $as_echo "$ac_cv_lib_curl_curl_multi_init" >&6; } if test "x$ac_cv_lib_curl_curl_multi_init" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LIBCURL 1 -_ACEOF - LIBS="-lcurl $LIBS" + +$as_echo "#define HAVE_LIBCURL 1" >>confdefs.h + + LIBCURL_LDLIBS=-lcurl + else as_fn_error $? "library 'curl' does not provide curl_multi_init" "$LINENO" 5 fi + pgac_save_CPPFLAGS=$CPPFLAGS + pgac_save_LDFLAGS=$LDFLAGS + pgac_save_LIBS=$LIBS + + CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" + LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS" + LIBS="$LIBCURL_LDLIBS $LIBS" + # Check to see whether the current platform supports threadsafe Curl # initialization. { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_global_init thread safety" >&5 @@ -12681,6 +12698,10 @@ $as_echo "$pgac_cv__libcurl_async_dns" >&6; } *** to use it with libpq." "$LINENO" 5 fi + CPPFLAGS=$pgac_save_CPPFLAGS + LDFLAGS=$pgac_save_LDFLAGS + LIBS=$pgac_save_LIBS + fi if test "$with_gssapi" = yes ; then @@ -14329,6 +14350,13 @@ done fi +if test "$with_libcurl" = yes ; then + # Error out early if this platform can't support libpq-oauth. + if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then + as_fn_error $? "client OAuth is not supported on this platform" "$LINENO" 5 + fi +fi + ## ## Types, structures, compiler characteristics ## diff --git a/configure.ac b/configure.ac index fc5f7475d07..218aeea1b3b 100644 --- a/configure.ac +++ b/configure.ac @@ -1033,19 +1033,27 @@ if test "$with_libcurl" = yes ; then # to explicitly set TLS 1.3 ciphersuites). PKG_CHECK_MODULES(LIBCURL, [libcurl >= 7.61.0]) - # We only care about -I, -D, and -L switches; - # note that -lcurl will be added by PGAC_CHECK_LIBCURL below. + # Curl's flags are kept separate from the standard CPPFLAGS/LDFLAGS. We use + # them only for libpq-oauth. + LIBCURL_CPPFLAGS= + LIBCURL_LDFLAGS= + + # We only care about -I, -D, and -L switches. Note that -lcurl will be added + # to LIBCURL_LDLIBS by PGAC_CHECK_LIBCURL, below. for pgac_option in $LIBCURL_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) LIBCURL_CPPFLAGS="$LIBCURL_CPPFLAGS $pgac_option";; esac done for pgac_option in $LIBCURL_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBCURL_LDFLAGS="$LIBCURL_LDFLAGS $pgac_option";; esac done + AC_SUBST(LIBCURL_CPPFLAGS) + AC_SUBST(LIBCURL_LDFLAGS) + # OAuth requires python for testing if test "$with_python" != yes; then AC_MSG_WARN([*** OAuth support tests require --with-python to run]) @@ -1340,9 +1348,6 @@ failure. It is possible the compiler isn't looking in the proper directory. Use --without-zlib to disable zlib support.])]) fi -# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults -# during gss_acquire_cred(). This is possibly related to Curl's Heimdal -# dependency on that platform? if test "$with_libcurl" = yes ; then PGAC_CHECK_LIBCURL fi @@ -1640,6 +1645,13 @@ if test "$PORTNAME" = "win32" ; then AC_CHECK_HEADERS(crtdefs.h) fi +if test "$with_libcurl" = yes ; then + # Error out early if this platform can't support libpq-oauth. + if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then + AC_MSG_ERROR([client OAuth is not supported on this platform]) + fi +fi + ## ## Types, structures, compiler characteristics ## diff --git a/meson.build b/meson.build index 27717ad8976..0f0df9b1af4 100644 --- a/meson.build +++ b/meson.build @@ -107,6 +107,7 @@ os_deps = [] backend_both_deps = [] backend_deps = [] libpq_deps = [] +libpq_oauth_deps = [] pg_sysroot = '' @@ -2587,6 +2588,7 @@ header_checks = [ 'xlocale.h', ] +header_macros = {} foreach header : header_checks varname = 'HAVE_' + header.underscorify().to_upper() @@ -2595,6 +2597,15 @@ foreach header : header_checks include_directories: postgres_inc, args: test_c_args) cdata.set(varname, found ? 1 : false, description: 'Define to 1 if you have the <@0@> header file.'.format(header)) + + # Mixing 1/false in cdata means we can't perform equality checks using + # cdata.get(), though, so store our defined header macros for later lookup. + # + # https://github.com/mesonbuild/meson/issues/11581 + # + if found + header_macros += {varname: true} + endif endforeach @@ -3251,17 +3262,18 @@ libpq_deps += [ gssapi, ldap_r, - # XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults - # during gss_acquire_cred(). This is possibly related to Curl's Heimdal - # dependency on that platform? - libcurl, libintl, ssl, ] +libpq_oauth_deps += [ + libcurl, +] + subdir('src/interfaces/libpq') -# fe_utils depends on libpq +# fe_utils and libpq-oauth depends on libpq subdir('src/fe_utils') +subdir('src/interfaces/libpq-oauth') # for frontend binaries frontend_code = declare_dependency( diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 737b2dd1869..eb9b5de75b4 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -343,6 +343,9 @@ perl_embed_ldflags = @perl_embed_ldflags@ AWK = @AWK@ LN_S = @LN_S@ +LIBCURL_CPPFLAGS = @LIBCURL_CPPFLAGS@ +LIBCURL_LDFLAGS = @LIBCURL_LDFLAGS@ +LIBCURL_LDLIBS = @LIBCURL_LDLIBS@ MSGFMT = @MSGFMT@ MSGFMT_FLAGS = @MSGFMT_FLAGS@ MSGMERGE = @MSGMERGE@ diff --git a/src/interfaces/Makefile b/src/interfaces/Makefile index 7d56b29d28f..e6822caa206 100644 --- a/src/interfaces/Makefile +++ b/src/interfaces/Makefile @@ -14,7 +14,19 @@ include $(top_builddir)/src/Makefile.global SUBDIRS = libpq ecpg +ifeq ($(with_libcurl), yes) +SUBDIRS += libpq-oauth +else +ALWAYS_SUBDIRS += libpq-oauth +endif + $(recurse) +$(recurse_always) all-ecpg-recurse: all-libpq-recurse install-ecpg-recurse: install-libpq-recurse + +ifeq ($(with_libcurl), yes) +all-libpq-oauth-recurse: all-libpq-recurse +install-libpq-oauth-recurse: install-libpq-recurse +endif diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile new file mode 100644 index 00000000000..5fd251a1d27 --- /dev/null +++ b/src/interfaces/libpq-oauth/Makefile @@ -0,0 +1,65 @@ +#------------------------------------------------------------------------- +# +# Makefile for libpq-oauth +# +# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1994, Regents of the University of California +# +# src/interfaces/libpq-oauth/Makefile +# +#------------------------------------------------------------------------- + +subdir = src/interfaces/libpq-oauth +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +PGFILEDESC = "libpq-oauth - device authorization OAuth support" + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. +NAME = pq-oauth-$(MAJORVERSION) + +# Force the name "libpq-oauth" for both the static and shared libraries. +override shlib := lib$(NAME)$(DLSUFFIX) +override stlib := lib$(NAME).a + +override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(LIBCURL_CPPFLAGS) $(CPPFLAGS) + +OBJS = \ + $(WIN32RES) \ + oauth-curl.o + +# The shared library needs additional glue symbols. +$(shlib): OBJS += oauth-utils.o +$(shlib): oauth-utils.o + +SHLIB_LINK_INTERNAL = $(libpq_pgport_shlib) +SHLIB_LINK = $(LIBCURL_LDFLAGS) $(LIBCURL_LDLIBS) +SHLIB_PREREQS = submake-libpq +SHLIB_EXPORTS = exports.txt + +# Disable -bundle_loader on macOS. +BE_DLLLIBS = + +# By default, a library without an SONAME doesn't get a static library, so we +# add it to the build explicitly. +all: all-lib all-static-lib + +# Shared library stuff +include $(top_srcdir)/src/Makefile.shlib + +# Ignore the standard rules for SONAME-less installation; we want both the +# static and shared libraries to go into libdir. +install: all installdirs $(stlib) $(shlib) + $(INSTALL_SHLIB) $(shlib) '$(DESTDIR)$(libdir)/$(shlib)' + $(INSTALL_STLIB) $(stlib) '$(DESTDIR)$(libdir)/$(stlib)' + +installdirs: + $(MKDIR_P) '$(DESTDIR)$(libdir)' + +uninstall: + rm -f '$(DESTDIR)$(libdir)/$(stlib)' + rm -f '$(DESTDIR)$(libdir)/$(shlib)' + +clean distclean: clean-lib + rm -f $(OBJS) oauth-utils.o diff --git a/src/interfaces/libpq-oauth/README b/src/interfaces/libpq-oauth/README new file mode 100644 index 00000000000..ef746617c71 --- /dev/null +++ b/src/interfaces/libpq-oauth/README @@ -0,0 +1,30 @@ +libpq-oauth is an optional module implementing the Device Authorization flow for +OAuth clients (RFC 8628). It was originally developed as part of libpq core and +later split out as its own shared library in order to isolate its dependency on +libcurl. (End users who don't want the Curl dependency can simply choose not to +install this module.) + +If a connection string allows the use of OAuth, the server asks for it, and a +libpq client has not installed its own custom OAuth flow, libpq will attempt to +delay-load this module using dlopen() and the following ABI. Failure to load +results in a failed connection. + += Load-Time ABI = + +This module ABI is an internal implementation detail, so it's subject to change +without warning, even during minor releases (however unlikely). The compiled +version of libpq-oauth should always match the compiled version of libpq. + +- PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +- void pg_fe_cleanup_oauth_flow(PGconn *conn); + +pg_fe_run_oauth_flow and pg_fe_cleanup_oauth_flow are implementations of +conn->async_auth and conn->cleanup_async_auth, respectively. + +- void libpq_oauth_init(pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + +At the moment, pg_fe_run_oauth_flow() relies on libpq's pg_g_threadlock and +libpq_gettext(), which must be injected by libpq before the flow is run. It also +relies on libpq to expose conn->errorMessage, via an errmsg_impl. diff --git a/src/interfaces/libpq-oauth/exports.txt b/src/interfaces/libpq-oauth/exports.txt new file mode 100644 index 00000000000..6891a83dbf9 --- /dev/null +++ b/src/interfaces/libpq-oauth/exports.txt @@ -0,0 +1,4 @@ +# src/interfaces/libpq-oauth/exports.txt +libpq_oauth_init 1 +pg_fe_run_oauth_flow 2 +pg_fe_cleanup_oauth_flow 3 diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build new file mode 100644 index 00000000000..7634b7f6fb1 --- /dev/null +++ b/src/interfaces/libpq-oauth/meson.build @@ -0,0 +1,54 @@ +# Copyright (c) 2022-2025, PostgreSQL Global Development Group + +oauth_flow_supported = ( + libcurl.found() + and (header_macros.has_key('HAVE_SYS_EVENT_H') + or header_macros.has_key('HAVE_SYS_EPOLL_H')) +) + +if libcurlopt.disabled() + subdir_done() +elif not oauth_flow_supported + if libcurlopt.enabled() + error('client OAuth is not supported on this platform') + endif + subdir_done() +endif + +libpq_oauth_sources = files( + 'oauth-curl.c', +) + +# The shared library needs additional glue symbols. +libpq_oauth_so_sources = files( + 'oauth-utils.c', +) + +export_file = custom_target('libpq-oauth.exports', + kwargs: gen_export_kwargs, +) + +# port needs to be in include path due to pthread-win32.h +libpq_oauth_inc = include_directories('.', '../libpq', '../../port') + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. +libpq_oauth_name = 'libpq-oauth-@0@'.format(pg_version_major) + +libpq_oauth_st = static_library(libpq_oauth_name, + libpq_oauth_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_pch: pch_postgres_fe_h, + dependencies: [frontend_stlib_code, libpq_oauth_deps], + kwargs: default_lib_args, +) + +libpq_oauth_so = shared_module(libpq_oauth_name, + libpq_oauth_sources + libpq_oauth_so_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_pch: pch_postgres_fe_h, + dependencies: [frontend_shlib_code, libpq, libpq_oauth_deps], + link_depends: export_file, + link_args: export_fmt.format(export_file.full_path()), + kwargs: default_lib_args, +) diff --git a/src/interfaces/libpq/fe-auth-oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c similarity index 98% rename from src/interfaces/libpq/fe-auth-oauth-curl.c rename to src/interfaces/libpq-oauth/oauth-curl.c index cd9c0323bb6..d52125415bc 100644 --- a/src/interfaces/libpq/fe-auth-oauth-curl.c +++ b/src/interfaces/libpq-oauth/oauth-curl.c @@ -1,6 +1,6 @@ /*------------------------------------------------------------------------- * - * fe-auth-oauth-curl.c + * oauth-curl.c * The libcurl implementation of OAuth/OIDC authentication, using the * OAuth Device Authorization Grant (RFC 8628). * @@ -8,7 +8,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * src/interfaces/libpq/fe-auth-oauth-curl.c + * src/interfaces/libpq-oauth/oauth-curl.c * *------------------------------------------------------------------------- */ @@ -17,20 +17,23 @@ #include <curl/curl.h> #include <math.h> -#ifdef HAVE_SYS_EPOLL_H +#include <unistd.h> + +#if defined(HAVE_SYS_EPOLL_H) #include <sys/epoll.h> #include <sys/timerfd.h> -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) #include <sys/event.h> +#else +#error libpq-oauth is not supported on this platform #endif -#include <unistd.h> #include "common/jsonapi.h" #include "fe-auth.h" #include "fe-auth-oauth.h" -#include "libpq-int.h" #include "mb/pg_wchar.h" +#include "oauth-curl.h" +#include "oauth-utils.h" /* * It's generally prudent to set a maximum response size to buffer in memory, @@ -1110,7 +1113,7 @@ parse_access_token(struct async_ctx *actx, struct token *tok) static bool setup_multiplexer(struct async_ctx *actx) { -#ifdef HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct epoll_event ev = {.events = EPOLLIN}; actx->mux = epoll_create1(EPOLL_CLOEXEC); @@ -1134,8 +1137,7 @@ setup_multiplexer(struct async_ctx *actx) } return true; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) actx->mux = kqueue(); if (actx->mux < 0) { @@ -1158,10 +1160,9 @@ setup_multiplexer(struct async_ctx *actx) } return true; +#else +#error setup_multiplexer is not implemented on this platform #endif - - actx_error(actx, "libpq does not support the Device Authorization flow on this platform"); - return false; } /* @@ -1174,7 +1175,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, { struct async_ctx *actx = ctx; -#ifdef HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct epoll_event ev = {0}; int res; int op = EPOLL_CTL_ADD; @@ -1230,8 +1231,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, } return 0; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) struct kevent ev[2] = {0}; struct kevent ev_out[2]; struct timespec timeout = {0}; @@ -1312,10 +1312,9 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, } return 0; +#else +#error register_socket is not implemented on this platform #endif - - actx_error(actx, "libpq does not support multiplexer sockets on this platform"); - return -1; } /* @@ -1334,7 +1333,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, static bool set_timer(struct async_ctx *actx, long timeout) { -#if HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct itimerspec spec = {0}; if (timeout < 0) @@ -1363,8 +1362,7 @@ set_timer(struct async_ctx *actx, long timeout) } return true; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) struct kevent ev; #ifdef __NetBSD__ @@ -1419,10 +1417,9 @@ set_timer(struct async_ctx *actx, long timeout) } return true; +#else +#error set_timer is not implemented on this platform #endif - - actx_error(actx, "libpq does not support timers on this platform"); - return false; } /* @@ -1433,7 +1430,7 @@ set_timer(struct async_ctx *actx, long timeout) static int timer_expired(struct async_ctx *actx) { -#if HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct itimerspec spec = {0}; if (timerfd_gettime(actx->timerfd, &spec) < 0) @@ -1453,8 +1450,7 @@ timer_expired(struct async_ctx *actx) /* If the remaining time to expiration is zero, we're done. */ return (spec.it_value.tv_sec == 0 && spec.it_value.tv_nsec == 0); -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) int res; /* Is the timer queue ready? */ @@ -1466,10 +1462,9 @@ timer_expired(struct async_ctx *actx) } return (res > 0); +#else +#error timer_expired is not implemented on this platform #endif - - actx_error(actx, "libpq does not support timers on this platform"); - return -1; } /* @@ -2487,8 +2482,9 @@ prompt_user(struct async_ctx *actx, PGconn *conn) .verification_uri_complete = actx->authz.verification_uri_complete, .expires_in = actx->authz.expires_in, }; + PQauthDataHook_type hook = PQgetAuthDataHook(); - res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); + res = hook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); if (!res) { diff --git a/src/interfaces/libpq-oauth/oauth-curl.h b/src/interfaces/libpq-oauth/oauth-curl.h new file mode 100644 index 00000000000..248d0424ad0 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-curl.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * oauth-curl.h + * + * Definitions for OAuth Device Authorization module + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-curl.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_CURL_H +#define OAUTH_CURL_H + +#include "libpq-fe.h" + +/* Exported async-auth callbacks. */ +extern PGDLLEXPORT PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +extern PGDLLEXPORT void pg_fe_cleanup_oauth_flow(PGconn *conn); + +#endif /* OAUTH_CURL_H */ diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c new file mode 100644 index 00000000000..2bdbf904743 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.c @@ -0,0 +1,202 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.c + * + * "Glue" helpers providing a copy of some internal APIs from libpq. At + * some point in the future, we might be able to deduplicate. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq-oauth/oauth-utils.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include <signal.h> + +#include "libpq-int.h" +#include "oauth-utils.h" + +static libpq_gettext_func libpq_gettext_impl; +static conn_errorMessage_func conn_errorMessage; + +pgthreadlock_t pg_g_threadlock; + +/*- + * Initializes libpq-oauth by setting necessary callbacks. + * + * The current implementation relies on the following private implementation + * details of libpq: + * + * - pg_g_threadlock: protects libcurl initialization if the underlying Curl + * installation is not threadsafe + * + * - libpq_gettext: translates error messages using libpq's message domain + * + * - conn->errorMessage: holds translated errors for the connection. This is + * handled through a translation shim, which avoids either depending on the + * offset of the errorMessage in PGconn, or needing to export the variadic + * libpq_append_conn_error(). + */ +void +libpq_oauth_init(pgthreadlock_t threadlock_impl, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl) +{ + pg_g_threadlock = threadlock_impl; + libpq_gettext_impl = gettext_impl; + conn_errorMessage = errmsg_impl; +} + +/* + * Append a formatted string to the error message buffer of the given + * connection, after translating it. This is a copy of libpq's internal API. + */ +void +libpq_append_conn_error(PGconn *conn, const char *fmt,...) +{ + int save_errno = errno; + bool done; + va_list args; + PQExpBuffer errorMessage = conn_errorMessage(conn); + + Assert(fmt[strlen(fmt) - 1] != '\n'); + + if (PQExpBufferBroken(errorMessage)) + return; /* already failed */ + + /* Loop in case we have to retry after enlarging the buffer. */ + do + { + errno = save_errno; + va_start(args, fmt); + done = appendPQExpBufferVA(errorMessage, libpq_gettext(fmt), args); + va_end(args); + } while (!done); + + appendPQExpBufferChar(errorMessage, '\n'); +} + +#ifdef ENABLE_NLS + +/* + * A shim that defers to the actual libpq_gettext(). + */ +char * +libpq_gettext(const char *msgid) +{ + if (!libpq_gettext_impl) + { + /* + * Possible if the libpq build doesn't enable NLS. That's a concerning + * mismatch, but in this particular case we can handle it. Try to warn + * a developer with an assertion, though. + */ + Assert(false); + + /* + * Note that callers of libpq_gettext() have to treat the return value + * as if it were const, because builds without NLS simply pass through + * their argument. + */ + return unconstify(char *, msgid); + } + + return libpq_gettext_impl(msgid); +} + +#endif /* ENABLE_NLS */ + +/* + * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment. + */ +bool +oauth_unsafe_debugging_enabled(void) +{ + const char *env = getenv("PGOAUTHDEBUG"); + + return (env && strcmp(env, "UNSAFE") == 0); +} + +/* + * Duplicate SOCK_ERRNO* definitions from libpq-int.h, for use by + * pq_block/reset_sigpipe(). + */ +#ifdef WIN32 +#define SOCK_ERRNO (WSAGetLastError()) +#define SOCK_ERRNO_SET(e) WSASetLastError(e) +#else +#define SOCK_ERRNO errno +#define SOCK_ERRNO_SET(e) (errno = (e)) +#endif + +/* + * Block SIGPIPE for this thread. This is a copy of libpq's internal API. + */ +int +pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending) +{ + sigset_t sigpipe_sigset; + sigset_t sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + /* Block SIGPIPE and save previous mask for later reset */ + SOCK_ERRNO_SET(pthread_sigmask(SIG_BLOCK, &sigpipe_sigset, osigset)); + if (SOCK_ERRNO) + return -1; + + /* We can have a pending SIGPIPE only if it was blocked before */ + if (sigismember(osigset, SIGPIPE)) + { + /* Is there a pending SIGPIPE? */ + if (sigpending(&sigset) != 0) + return -1; + + if (sigismember(&sigset, SIGPIPE)) + *sigpipe_pending = true; + else + *sigpipe_pending = false; + } + else + *sigpipe_pending = false; + + return 0; +} + +/* + * Discard any pending SIGPIPE and reset the signal mask. This is a copy of + * libpq's internal API. + */ +void +pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe) +{ + int save_errno = SOCK_ERRNO; + int signo; + sigset_t sigset; + + /* Clear SIGPIPE only if none was pending */ + if (got_epipe && !sigpipe_pending) + { + if (sigpending(&sigset) == 0 && + sigismember(&sigset, SIGPIPE)) + { + sigset_t sigpipe_sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + sigwait(&sigpipe_sigset, &signo); + } + } + + /* Restore saved block mask */ + pthread_sigmask(SIG_SETMASK, osigset, NULL); + + SOCK_ERRNO_SET(save_errno); +} diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h new file mode 100644 index 00000000000..279fc113248 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.h @@ -0,0 +1,35 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.h + * + * Definitions providing missing libpq internal APIs + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-utils.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_UTILS_H +#define OAUTH_UTILS_H + +#include "libpq-fe.h" +#include "pqexpbuffer.h" + +typedef char *(*libpq_gettext_func) (const char *msgid); +typedef PQExpBuffer (*conn_errorMessage_func) (PGconn *conn); + +/* Initializes libpq-oauth. */ +extern PGDLLEXPORT void libpq_oauth_init(pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + +/* Duplicated APIs, copied from libpq. */ +extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3); +extern bool oauth_unsafe_debugging_enabled(void); +extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending); +extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe); + +#endif /* OAUTH_UTILS_H */ diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 90b0b65db6f..852b1948ecb 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -64,10 +64,6 @@ OBJS += \ fe-secure-gssapi.o endif -ifeq ($(with_libcurl),yes) -OBJS += fe-auth-oauth-curl.o -endif - ifeq ($(PORTNAME), cygwin) override shlib = cyg$(NAME)$(DLSUFFIX) endif @@ -86,7 +82,7 @@ endif # that are built correctly for use in a shlib. SHLIB_LINK_INTERNAL = -lpgcommon_shlib -lpgport_shlib ifneq ($(PORTNAME), win32) -SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lcurl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) +SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) else SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi32 -lssl -lsocket -lnsl -lresolv -lintl -lm $(PTHREAD_LIBS), $(LIBS)) $(LDAP_LIBS_FE) endif @@ -107,6 +103,15 @@ all: all-lib libpq-refs-stamp include $(top_srcdir)/src/Makefile.shlib backend_src = $(top_srcdir)/src/backend +# Add the correct implementations of the OAuth flow, if requested, for both +# shared and static builds. +ifeq ($(with_libcurl),yes) +$(shlib): OBJS += fe-auth-oauth-dynamic.o +$(shlib): fe-auth-oauth-dynamic.o +$(stlib): OBJS += fe-auth-oauth-static.o +$(stlib): fe-auth-oauth-static.o +endif + # Check for functions that libpq must not call, currently just exit(). # (Ideally we'd reject abort() too, but there are various scenarios where # build toolchains insert abort() calls, e.g. to implement assert().) @@ -115,8 +120,6 @@ backend_src = $(top_srcdir)/src/backend # which seems to insert references to that even in pure C code. Excluding # __tsan_func_exit is necessary when using ThreadSanitizer data race detector # which use this function for instrumentation of function exit. -# libcurl registers an exit handler in the memory debugging code when running -# with LeakSanitizer. # Skip the test when profiling, as gcc may insert exit() calls for that. # Also skip the test on platforms where libpq infrastructure may be provided # by statically-linked libraries, as we can't expect them to honor this @@ -124,7 +127,7 @@ backend_src = $(top_srcdir)/src/backend libpq-refs-stamp: $(shlib) ifneq ($(enable_coverage), yes) ifeq (,$(filter solaris,$(PORTNAME))) - @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit -e _atexit | grep exit; then \ + @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit | grep exit; then \ echo 'libpq must not be calling any function which invokes exit'; exit 1; \ fi endif @@ -172,5 +175,6 @@ clean distclean: clean-lib $(MAKE) -C test $@ rm -rf tmp_check rm -f $(OBJS) pthread.h libpq-refs-stamp + rm -f fe-auth-oauth-dynamic.o fe-auth-oauth-static.o # Might be left over from a Win32 client-only build rm -f pg_config_paths.h diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index d5143766858..0625cf39e9a 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -210,3 +210,4 @@ PQsetAuthDataHook 207 PQgetAuthDataHook 208 PQdefaultAuthDataHook 209 PQfullProtocolVersion 210 +appendPQExpBufferVA 211 diff --git a/src/interfaces/libpq/fe-auth-oauth-dynamic.c b/src/interfaces/libpq/fe-auth-oauth-dynamic.c new file mode 100644 index 00000000000..a84551a6307 --- /dev/null +++ b/src/interfaces/libpq/fe-auth-oauth-dynamic.c @@ -0,0 +1,109 @@ +/*------------------------------------------------------------------------- + * + * fe-auth-oauth-dynamic.c + * + * Implements the builtin flow by loading the libpq-oauth plugin. + * See also fe-auth-oauth-static.c, for static builds. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq/fe-auth-oauth-dynamic.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#ifndef USE_LIBCURL +#error this should only be compiled when OAuth support is enabled +#endif + +#include <dlfcn.h> + +#include "fe-auth-oauth.h" + +typedef char *(*libpq_gettext_func) (const char *msgid); +typedef PQExpBuffer (*conn_errorMessage_func) (PGconn *conn); + +/* + * This shim is injected into libpq-oauth so that it doesn't depend on the + * offset of conn->errorMessage. + * + * TODO: look into exporting libpq_append_conn_error or a comparable API from + * libpq, instead. + */ +static PQExpBuffer +conn_errorMessage(PGconn *conn) +{ + return &conn->errorMessage; +} + +/* + * 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) +{ + void (*init) (pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + PostgresPollingStatusType (*flow) (PGconn *conn); + void (*cleanup) (PGconn *conn); + + state->builtin_flow = dlopen("libpq-oauth-" PG_MAJORVERSION DLSUFFIX, + 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; + } + + /* + * Inject necessary function pointers into the module. + */ + init(pg_g_threadlock, +#ifdef ENABLE_NLS + libpq_gettext, +#else + NULL, +#endif + conn_errorMessage); + + /* Set our asynchronous callbacks. */ + conn->async_auth = flow; + conn->cleanup_async_auth = cleanup; + + return true; +} diff --git a/src/interfaces/libpq/fe-auth-oauth-static.c b/src/interfaces/libpq/fe-auth-oauth-static.c new file mode 100644 index 00000000000..25119bbb50c --- /dev/null +++ b/src/interfaces/libpq/fe-auth-oauth-static.c @@ -0,0 +1,40 @@ +/*------------------------------------------------------------------------- + * + * fe-auth-oauth-static.c + * + * Implements the builtin flow using the libpq-oauth.a staticlib. + * See also fe-auth-oauth-dynamic.c, which loads a plugin at runtime. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq/fe-auth-oauth-static.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#ifndef USE_LIBCURL +#error this should only be compiled when OAuth support is enabled +#endif + +#include "fe-auth-oauth.h" + +/* see libpq-oauth/oauth-curl.h */ +extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +extern void pg_fe_cleanup_oauth_flow(PGconn *conn); + +/* + * Loads the builtin flow from libpq-oauth.a. + */ +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; +} diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c index cf1a25e2ccc..0bad132b580 100644 --- a/src/interfaces/libpq/fe-auth-oauth.c +++ b/src/interfaces/libpq/fe-auth-oauth.c @@ -22,6 +22,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 +722,21 @@ cleanup_user_oauth_flow(PGconn *conn) state->async_ctx = NULL; } +#ifndef USE_LIBCURL + +/* + * This configuration doesn't support the builtin flow. + * + * Alternative implementations are in fe-auth-oauth-dynamic/-static.c. + */ +bool +use_builtin_flow(PGconn *conn, fe_oauth_state *state) +{ + return false; +} + +#endif + /* * Chooses an OAuth client flow for the connection, which will retrieve a Bearer * token for presentation to the server. @@ -792,18 +808,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"); goto fail; - -#endif } return true; diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h index 3f1a7503a01..687e664475f 100644 --- a/src/interfaces/libpq/fe-auth-oauth.h +++ b/src/interfaces/libpq/fe-auth-oauth.h @@ -33,12 +33,13 @@ typedef struct PGconn *conn; void *async_ctx; + + void *builtin_flow; } fe_oauth_state; -extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); -extern void pg_fe_cleanup_oauth_flow(PGconn *conn); extern void pqClearOAuthToken(PGconn *conn); extern bool oauth_unsafe_debugging_enabled(void); +extern bool use_builtin_flow(PGconn *conn, fe_oauth_state *state); /* Mechanisms in fe-auth-oauth.c */ extern const pg_fe_sasl_mech pg_oauth_mech; diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build index 292fecf3320..bf880e053f8 100644 --- a/src/interfaces/libpq/meson.build +++ b/src/interfaces/libpq/meson.build @@ -18,6 +18,7 @@ libpq_sources = files( 'pqexpbuffer.c', ) libpq_so_sources = [] # for shared lib, in addition to the above +libpq_st_sources = [] # for static lib, in addition to the above if host_system == 'windows' libpq_sources += files('pthread-win32.c', 'win32.c') @@ -38,8 +39,11 @@ if gssapi.found() ) endif +# Add the correct implementations of the OAuth flow, if requested, for both +# shared and static builds. if libcurl.found() - libpq_sources += files('fe-auth-oauth-curl.c') + libpq_so_sources += files('fe-auth-oauth-dynamic.c') + libpq_st_sources += files('fe-auth-oauth-static.c') endif export_file = custom_target('libpq.exports', @@ -59,7 +63,7 @@ libpq_c_args = ['-DSO_MAJOR_VERSION=5'] # more complexity than its worth (reusing object files requires also linking # to the library on windows or breaks precompiled headers). libpq_st = static_library('libpq', - libpq_sources, + libpq_sources + libpq_st_sources, include_directories: [libpq_inc], c_args: libpq_c_args, c_pch: pch_postgres_fe_h, diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk index ae761265852..b87df277d93 100644 --- a/src/interfaces/libpq/nls.mk +++ b/src/interfaces/libpq/nls.mk @@ -13,15 +13,21 @@ GETTEXT_FILES = fe-auth.c \ fe-secure-common.c \ fe-secure-gssapi.c \ fe-secure-openssl.c \ - win32.c -GETTEXT_TRIGGERS = libpq_append_conn_error:2 \ + win32.c \ + ../libpq-oauth/oauth-curl.c \ + ../libpq-oauth/oauth-utils.c +GETTEXT_TRIGGERS = actx_error:2 \ + libpq_append_conn_error:2 \ libpq_append_error:2 \ libpq_gettext \ libpq_ngettext:1,2 \ + oauth_parse_set_error:2 \ pqInternalNotice:2 -GETTEXT_FLAGS = libpq_append_conn_error:2:c-format \ +GETTEXT_FLAGS = actx_error:2:c-format \ + libpq_append_conn_error:2:c-format \ libpq_append_error:2:c-format \ libpq_gettext:1:pass-c-format \ libpq_ngettext:1:pass-c-format \ libpq_ngettext:2:pass-c-format \ + oauth_parse_set_error:2:c-format \ pqInternalNotice:2:c-format diff --git a/src/makefiles/meson.build b/src/makefiles/meson.build index 46d8da070e8..f2ba5b38124 100644 --- a/src/makefiles/meson.build +++ b/src/makefiles/meson.build @@ -201,6 +201,8 @@ pgxs_empty = [ 'ICU_LIBS', 'LIBURING_CFLAGS', 'LIBURING_LIBS', + + 'LIBCURL_CPPFLAGS', 'LIBCURL_LDFLAGS', 'LIBCURL_LDLIBS', ] if host_system == 'windows' and cc.get_argument_syntax() != 'msvc' -- 2.34.1 ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-15 12:31 Peter Eisentraut <[email protected]> parent: Jacob Champion <[email protected]> 1 sibling, 3 replies; 36+ messages in thread From: Peter Eisentraut @ 2025-04-15 12:31 UTC (permalink / raw) To: Jacob Champion <[email protected]>; Christoph Berg <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]>; Jelte Fennema-Nio <[email protected]> On 10.04.25 01:08, Jacob Champion wrote: > Christoph noted that this was also confusing from the packaging side, > earlier, and Daniel proposed -Doauth-client/--with-oauth-client as the > feature switch name instead. Any objections? Unfortunately it would > mean a buildfarm email is in order, so we should get it locked in. We had that discussion early in the development, and I still think it's not the right choice. The general idea, at least on the Autoconf side, is that --with-FOO means enable all the features that require library FOO. For example, --with-ldap enables all the LDAP-related features, including authentication support in libpq, authentication support in the server, and service lookup in libpq. --with-[open]ssl enables all the features that use OpenSSL, including SSL support in the client and server but also encryption support in pgcrypto. The naming system you propose has problems: First, what if we add another kind of "oauth-client" that doesn't use libcurl, how would you extend the set of options? Second, what if we add some kind of oauth plugin for the server that uses libcurl, how would you extend the set of options? If you used that system for options in the ldap or openssl cases, you'd end up with maybe six options (and packagers would forget to turn on half of them). But worse, what you are hiding is the information what dependencies you are pulling in, which is the actual reason for the options. (If there was no external dependency, there would be no option at all.) This seems unnecessarily complicated and inconsistent. Once you have made the choice of taking the libcurl dependency, why not build everything that requires it? (Nitpick: If you go with this kind of option, it should be --enable-XXX on the Autoconf side.) ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-15 15:34 Christoph Berg <[email protected]> parent: Peter Eisentraut <[email protected]> 2 siblings, 1 reply; 36+ messages in thread From: Christoph Berg @ 2025-04-15 15:34 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Jacob Champion <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]>; Jelte Fennema-Nio <[email protected]> Re: Peter Eisentraut > But worse, what you are hiding is the information what dependencies > you are pulling in, which is the actual reason for the options. (If there > was no external dependency, there would be no option at all.) > > This seems unnecessarily complicated and inconsistent. Once you have made > the choice of taking the libcurl dependency, why not build everything that > requires it? I agree with this reasoning and retract my suggestion to rename the option. Thanks for the explanation, Christoph ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-15 17:53 Jacob Champion <[email protected]> parent: Peter Eisentraut <[email protected]> 2 siblings, 2 replies; 36+ messages in thread From: Jacob Champion @ 2025-04-15 17:53 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Christoph Berg <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]>; Jelte Fennema-Nio <[email protected]> On Tue, Apr 15, 2025 at 5:31 AM Peter Eisentraut <[email protected]> wrote: > On 10.04.25 01:08, Jacob Champion wrote: > > Christoph noted that this was also confusing from the packaging side, > > earlier, Since Christoph has withdrawn the request, I will drop -0002. However, I'll go ahead and put some of my opinions down on paper here: > The general idea, at least on the Autoconf side, is that --with-FOO > means enable all the features that require library FOO. I don't think this is particularly user-friendly if it's not obvious what feature is enabled by FOO. LDAP? PAM? Sure. SSL? Eh, I think the pgcrypto coupling is a little strange -- that's not implied by "SSL" at all! -- but it's not problematic enough to complain loudly. --with-gssapi selects... some dependency... which may or may not come from a particular library. --with-bsd-auth doesn't add any library dependencies at all, instead depending on the kernel, but it makes sense. But there's no connection between "libcurl" and "OAuth Device Authorization flow" in anyone's mind except the people who have worked on that feature. If the argument is that we'd need to switch to --enable-oauth-client rather than --with-oauth-client, that works for me. But I don't quite understand the desire to stick to the existing configuration methodology for something that's very different from an end-user perspective. > The naming system you propose has problems: > > First, what if we add another kind of "oauth-client" that doesn't use > libcurl, how would you extend the set of options? With an extension to the values that you can provide to --with-oauth-client, similarly to what was originally proposed for --with-ssl. > Second, what if we add some kind of oauth plugin for the server that > uses libcurl, how would you extend the set of options? With a new option. But let me turn this around, because we currently have the opposite problem: if someone comes in and adds a completely new feature depending on libcurl, and you want OAuth but you do not want that new feature -- or vice-versa -- what do you do? In other words, what if your concern is not with libcurl, but with the feature itself? > But worse, what you are hiding is the information what > dependencies you are pulling in, which is the actual reason for the > options. (If there was no external dependency, there would be no option > at all.) I'm not sure I agree, either practically or philosophically. I like to see the build dependencies, definitely, but I also like to see the features. (Meson will make both things visible separately, for that matter.) > This seems unnecessarily complicated and inconsistent. Once you have > made the choice of taking the libcurl dependency, why not build > everything that requires it? Simply because the end user or packager might not want to. In any case -- I won't die on this particular hill, and I'm happy to continue forward with 0001 alone. Thanks! --Jacob ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-15 18:02 Jacob Champion <[email protected]> parent: Christoph Berg <[email protected]> 0 siblings, 0 replies; 36+ messages in thread From: Jacob Champion @ 2025-04-15 18:02 UTC (permalink / raw) To: Christoph Berg <[email protected]>; Peter Eisentraut <[email protected]>; Jacob Champion <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]>; Jelte Fennema-Nio <[email protected]> On Tue, Apr 15, 2025 at 8:34 AM Christoph Berg <[email protected]> wrote: > I agree with this reasoning and retract my suggestion to rename the option. (Thank you for chiming in; having the packager feedback has been extremely helpful.) While I have you, may I ask whether you're okay (from the packager perspective) with the current division of dynamic and static behaviors? Dynamic: --with-libcurl builds a runtime-loadable module, and if you don't install it, OAuth isn't supported (i.e. it's optional) Static: --with-libcurl builds an additional linkable staticlib, which you must link into your application (i.e. not optional) I want to make absolutely sure the existing packager requests are not conflicting. :D Thanks, --Jacob ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-15 18:57 Christoph Berg <[email protected]> parent: Jacob Champion <[email protected]> 1 sibling, 1 reply; 36+ messages in thread From: Christoph Berg @ 2025-04-15 18:57 UTC (permalink / raw) To: Jacob Champion <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]>; Jelte Fennema-Nio <[email protected]> Re: Jacob Champion > But there's no connection between "libcurl" and "OAuth Device > Authorization flow" in anyone's mind except the people who have worked > on that feature. Fwiw that was exactly the reason I originally voiced the idea to rename. > But let me turn this around, because we currently have the opposite > problem: if someone comes in and adds a completely new feature > depending on libcurl, and you want OAuth but you do not want that new > feature -- or vice-versa -- what do you do? In other words, what if > your concern is not with libcurl, but with the feature itself? What made me reconsider was Peter saying that what defines the blast radius of some feature is usually the extra dependency pulled in. If you don't like tracking OpenSSL problems, build without it. If you don't like libcurl, build without it. That's the "we are going to be hated by security scanner people" argument that brought up this sub-thread. Now if the feature itself were a problem, that might change how configuration should be working. Is "libpq can now initiate oauth requests" something people would like to be able to control? Re: Jacob Champion > Dynamic: --with-libcurl builds a runtime-loadable module, and if you > don't install it, OAuth isn't supported (i.e. it's optional) Ok. > Static: --with-libcurl builds an additional linkable staticlib, which > you must link into your application (i.e. not optional) Debian does not care really about static libs. We are currently shipping libpq.a, but if it breaks in any funny way, we might as well remove it. Christoph ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-15 19:21 Robert Haas <[email protected]> parent: Peter Eisentraut <[email protected]> 2 siblings, 1 reply; 36+ messages in thread From: Robert Haas @ 2025-04-15 19:21 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Jacob Champion <[email protected]>; Christoph Berg <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]>; Jelte Fennema-Nio <[email protected]> On Tue, Apr 15, 2025 at 8:32 AM Peter Eisentraut <[email protected]> wrote: > On 10.04.25 01:08, Jacob Champion wrote: > > Christoph noted that this was also confusing from the packaging side, > > earlier, and Daniel proposed -Doauth-client/--with-oauth-client as the > > feature switch name instead. Any objections? Unfortunately it would > > mean a buildfarm email is in order, so we should get it locked in. > > We had that discussion early in the development, and I still think it's > not the right choice. I strongly agree. I think it will not be long before somebody implements a second feature depending on libcurl, and there's no precedent for the idea that we allow those features to be enabled and disabled individually. If that turns out to be something that is wanted, then since this will be a separate library, a packager can choose not to ship it, or to put it in a separate package, if they wish. If there's REALLY a lot of demand for a separate enable/disable switch for this feature then we can consider making this an exception to what we do for all other dependent libraries, but I bet there won't be. I can imagine someone not wanting libcurl on their system on the theory that it would potentially open up the ability to download data from arbitrary URLs which might be considered bad from a security posture -- but I don't really see why someone would be particularly upset about one particular way in which libcurl might be used. I also don't mind being wrong, of course. But I think it's better to bet on making this like other things and then change strategy if that doesn't work out, rather than starting out by making this different from other things. -- Robert Haas EDB: http://www.enterprisedb.com ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-15 19:44 Jacob Champion <[email protected]> parent: Christoph Berg <[email protected]> 0 siblings, 0 replies; 36+ messages in thread From: Jacob Champion @ 2025-04-15 19:44 UTC (permalink / raw) To: Christoph Berg <[email protected]>; Jacob Champion <[email protected]>; Peter Eisentraut <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]>; Jelte Fennema-Nio <[email protected]> On Tue, Apr 15, 2025 at 11:57 AM Christoph Berg <[email protected]> wrote: > What made me reconsider was Peter saying that what defines the blast > radius of some feature is usually the extra dependency pulled in. If > you don't like tracking OpenSSL problems, build without it. If you > don't like libcurl, build without it. That's the "we are going to be > hated by security scanner people" argument that brought up this > sub-thread. > > Now if the feature itself were a problem, that might change how > configuration should be working. Is "libpq can now initiate oauth > requests" something people would like to be able to control? Well... I'd sure like to live in a world where users thought about the implications and risks of what they're using and why, rather than farming a decision out to a static analysis tool. ("And as long as I'm dreaming, I'd like a pony.") But end users already control the initiation of OAuth requests (it's opt-in via the connection string), so that's not really the goal. > Debian does not care really about static libs. We are currently > shipping libpq.a, but if it breaks in any funny way, we might as well > remove it. Awesome. I think we have a consensus. Thanks! --Jacob ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-15 19:45 Jacob Champion <[email protected]> parent: Robert Haas <[email protected]> 0 siblings, 0 replies; 36+ messages in thread From: Jacob Champion @ 2025-04-15 19:45 UTC (permalink / raw) To: Robert Haas <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Christoph Berg <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]>; Jelte Fennema-Nio <[email protected]> On Tue, Apr 15, 2025 at 12:21 PM Robert Haas <[email protected]> wrote: > I also don't mind being wrong, of course. But I think it's better to > bet on making this like other things and then change strategy if that > doesn't work out, rather than starting out by making this different > from other things. Works for me. (And it's less work, too!) Thanks, --Jacob ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-15 21:38 Jelte Fennema-Nio <[email protected]> parent: Jacob Champion <[email protected]> 1 sibling, 1 reply; 36+ messages in thread From: Jelte Fennema-Nio @ 2025-04-15 21:38 UTC (permalink / raw) To: Jacob Champion <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Christoph Berg <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]> On Tue, 15 Apr 2025 at 19:53, Jacob Champion <[email protected]> wrote: > But let me turn this around, because we currently have the opposite > problem: if someone comes in and adds a completely new feature > depending on libcurl, and you want OAuth but you do not want that new > feature -- or vice-versa -- what do you do? In other words, what if > your concern is not with libcurl, but with the feature itself? After reconsidering this, I now agree with Peter and Robert that --with-libcurl is the flag that we should be relying on. Specifically because of the situation you're describing above: Once you have libcurl, why wouldn't you want all the features (e.g. in some other thread there was a suggestion about fetching the PGSERVICEFILE from a HTTP endpoint). It's not like we add compile time flags for other user facing features like --enable-index-scan. All the --enable-xyz options that we have are for developer features (like debug and asserts). Starting to add such a flag for this feature seems unnecessary. Regarding discoverability, I think the error message that you have already solves that: > libpq_append_conn_error(conn, "no custom OAuth flows are available, and libpq was not built with libcurl support"); Side-note: I think it would be good to have a different error when libpq was build with libcurl support, but the dlopen failed. Something like: libpq_append_conn_error(conn, "no custom OAuth flows are available, and libpq-oauth could not be loaded library could not be loaded. Try installing the libpq-oauth package from the same source that you installed libpq from"); ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-18 00:47 Jacob Champion <[email protected]> parent: Jelte Fennema-Nio <[email protected]> 0 siblings, 2 replies; 36+ messages in thread From: Jacob Champion @ 2025-04-18 00:47 UTC (permalink / raw) To: Jelte Fennema-Nio <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Christoph Berg <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]> On Tue, Apr 15, 2025 at 2:38 PM Jelte Fennema-Nio <[email protected]> wrote: > libpq_append_conn_error(conn, "no custom OAuth flows are available, > and libpq-oauth could not be loaded library could not be loaded. Try > installing the libpq-oauth package from the same source that you > installed libpq from"); Thanks! I think that's a little too prescriptive for packagers, personally, but I agree that the current message isn't correct anymore. I've gone with "no custom OAuth flows are available, and the builtin flow is not installed". (I suppose packagers could patch in a platform-specific message if they really wanted?) -- Other changes in v7: - The option name remains --with-libcurl. - Daniel and I have tweaked the documentation, and a draft commit message is up - Removed the ENABLE_NLS-mismatch assertion in oauth-utils.c; we don't need to care anymore - Added an initialization mutex I was feeling paranoid about injecting dependency pointers concurrently to their use in another thread. They're _supposed_ to be constant... but I have no doubt that someone somewhere knows of a platform/compiler/linker combo where that blows up anyway. Initialization is now run once, under pthread_mutex protection. - Fixed module load on macOS The green CI was masking a bug with its use of DYLD_LIBRARY_PATH: we don't make use of RPATH on macOS, so after installing libpq, it lost the ability to find libpq-oauth. (A stale installation due to SIP weirdness was masking this on my local machine; sorry for not catching it before.) I have swapped to using an absolute path on Mac only, because unlike LD_LIBRARY_PATH on *nix, DYLD_LIBRARY_PATH can still override absolute paths in dlopen()! Whee. I could use a sanity check from a native Mac developer, but I believe this mirrors the expected behavior for a "typical" runtime dependency: libraries point directly to the things they depend on. With those, I have no more TODOs and I believe this is ready for a final review round. Thanks, --Jacob Attachments: [application/octet-stream] v7-0001-oauth-Move-the-builtin-flow-into-a-separate-modul.patch (53.7K, ../../CAOYmi+=j9nLQFjQ8z0vyQmuhNMwsFbzvne_2S2pTbBGir4q6EQ@mail.gmail.com/2-v7-0001-oauth-Move-the-builtin-flow-into-a-separate-modul.patch) download | inline diff: From 942ad5391e2acbb143ffcfec3d5bf8023d4a17ad Mon Sep 17 00:00:00 2001 From: Jacob Champion <[email protected]> Date: Wed, 26 Mar 2025 10:55:28 -0700 Subject: [PATCH v7] 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 offset of conn->errorMessage to libpq-oauth, so that we can function without crashing if the module on the search path came from a different build of Postgres. 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 major 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 many people reviewed the design and implementation; thank you! Co-authored-by: Daniel Gustafsson <[email protected]> Reviewed-by: Andres Freund <[email protected]> Reviewed-by: Christoph Berg <[email protected]> Reviewed-by: Jelte Fennema-Nio <[email protected]> Reviewed-by: Peter Eisentraut <[email protected]> Reviewed-by: Wolfgang Walther <[email protected]> Discussion: https://postgr.es/m/641687.1742360249%40sss.pgh.pa.us --- config/programs.m4 | 17 +- configure | 50 ++++- configure.ac | 26 ++- doc/src/sgml/installation.sgml | 8 + doc/src/sgml/libpq.sgml | 30 ++- meson.build | 32 ++- src/Makefile.global.in | 3 + src/interfaces/Makefile | 12 ++ src/interfaces/libpq-oauth/Makefile | 65 ++++++ src/interfaces/libpq-oauth/README | 43 ++++ src/interfaces/libpq-oauth/exports.txt | 4 + src/interfaces/libpq-oauth/meson.build | 43 ++++ .../oauth-curl.c} | 60 +++--- src/interfaces/libpq-oauth/oauth-curl.h | 24 +++ src/interfaces/libpq-oauth/oauth-utils.c | 198 ++++++++++++++++++ src/interfaces/libpq-oauth/oauth-utils.h | 35 ++++ src/interfaces/libpq/Makefile | 36 +++- src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-auth-oauth.c | 197 ++++++++++++++++- src/interfaces/libpq/fe-auth-oauth.h | 5 +- src/interfaces/libpq/meson.build | 25 ++- src/interfaces/libpq/nls.mk | 12 +- src/makefiles/meson.build | 2 + src/test/modules/oauth_validator/meson.build | 2 +- .../modules/oauth_validator/t/002_client.pl | 2 +- 25 files changed, 834 insertions(+), 98 deletions(-) create mode 100644 src/interfaces/libpq-oauth/Makefile create mode 100644 src/interfaces/libpq-oauth/README create mode 100644 src/interfaces/libpq-oauth/exports.txt create mode 100644 src/interfaces/libpq-oauth/meson.build rename src/interfaces/{libpq/fe-auth-oauth-curl.c => libpq-oauth/oauth-curl.c} (98%) create mode 100644 src/interfaces/libpq-oauth/oauth-curl.h create mode 100644 src/interfaces/libpq-oauth/oauth-utils.c create mode 100644 src/interfaces/libpq-oauth/oauth-utils.h diff --git a/config/programs.m4 b/config/programs.m4 index 0a07feb37cc..0ad1e58b48d 100644 --- a/config/programs.m4 +++ b/config/programs.m4 @@ -286,9 +286,20 @@ AC_DEFUN([PGAC_CHECK_LIBCURL], [ AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file <curl/curl.h> is required for --with-libcurl])]) - AC_CHECK_LIB(curl, curl_multi_init, [], + AC_CHECK_LIB(curl, curl_multi_init, [ + AC_DEFINE([HAVE_LIBCURL], [1], [Define to 1 if you have the `curl' library (-lcurl).]) + AC_SUBST(LIBCURL_LDLIBS, -lcurl) + ], [AC_MSG_ERROR([library 'curl' does not provide curl_multi_init])]) + pgac_save_CPPFLAGS=$CPPFLAGS + pgac_save_LDFLAGS=$LDFLAGS + pgac_save_LIBS=$LIBS + + CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" + LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS" + LIBS="$LIBCURL_LDLIBS $LIBS" + # Check to see whether the current platform supports threadsafe Curl # initialization. AC_CACHE_CHECK([for curl_global_init thread safety], [pgac_cv__libcurl_threadsafe_init], @@ -338,4 +349,8 @@ AC_DEFUN([PGAC_CHECK_LIBCURL], *** lookups. Rebuild libcurl with the AsynchDNS feature enabled in order *** to use it with libpq.]) fi + + CPPFLAGS=$pgac_save_CPPFLAGS + LDFLAGS=$pgac_save_LDFLAGS + LIBS=$pgac_save_LIBS ])# PGAC_CHECK_LIBCURL diff --git a/configure b/configure index 0936010718d..a4c4bcb40ea 100755 --- a/configure +++ b/configure @@ -655,6 +655,7 @@ UUID_LIBS LDAP_LIBS_BE LDAP_LIBS_FE with_ssl +LIBCURL_LDLIBS PTHREAD_CFLAGS PTHREAD_LIBS PTHREAD_CC @@ -711,6 +712,8 @@ with_libxml LIBNUMA_LIBS LIBNUMA_CFLAGS with_libnuma +LIBCURL_LDFLAGS +LIBCURL_CPPFLAGS LIBCURL_LIBS LIBCURL_CFLAGS with_libcurl @@ -9053,19 +9056,27 @@ $as_echo "yes" >&6; } fi - # We only care about -I, -D, and -L switches; - # note that -lcurl will be added by PGAC_CHECK_LIBCURL below. + # Curl's flags are kept separate from the standard CPPFLAGS/LDFLAGS. We use + # them only for libpq-oauth. + LIBCURL_CPPFLAGS= + LIBCURL_LDFLAGS= + + # We only care about -I, -D, and -L switches. Note that -lcurl will be added + # to LIBCURL_LDLIBS by PGAC_CHECK_LIBCURL, below. for pgac_option in $LIBCURL_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) LIBCURL_CPPFLAGS="$LIBCURL_CPPFLAGS $pgac_option";; esac done for pgac_option in $LIBCURL_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBCURL_LDFLAGS="$LIBCURL_LDFLAGS $pgac_option";; esac done + + + # OAuth requires python for testing if test "$with_python" != yes; then { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: *** OAuth support tests require --with-python to run" >&5 @@ -12704,9 +12715,6 @@ fi fi -# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults -# during gss_acquire_cred(). This is possibly related to Curl's Heimdal -# dependency on that platform? if test "$with_libcurl" = yes ; then ac_fn_c_check_header_mongrel "$LINENO" "curl/curl.h" "ac_cv_header_curl_curl_h" "$ac_includes_default" @@ -12754,17 +12762,26 @@ fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curl_curl_multi_init" >&5 $as_echo "$ac_cv_lib_curl_curl_multi_init" >&6; } if test "x$ac_cv_lib_curl_curl_multi_init" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LIBCURL 1 -_ACEOF - LIBS="-lcurl $LIBS" + +$as_echo "#define HAVE_LIBCURL 1" >>confdefs.h + + LIBCURL_LDLIBS=-lcurl + else as_fn_error $? "library 'curl' does not provide curl_multi_init" "$LINENO" 5 fi + pgac_save_CPPFLAGS=$CPPFLAGS + pgac_save_LDFLAGS=$LDFLAGS + pgac_save_LIBS=$LIBS + + CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" + LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS" + LIBS="$LIBCURL_LDLIBS $LIBS" + # Check to see whether the current platform supports threadsafe Curl # initialization. { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_global_init thread safety" >&5 @@ -12868,6 +12885,10 @@ $as_echo "$pgac_cv__libcurl_async_dns" >&6; } *** to use it with libpq." "$LINENO" 5 fi + CPPFLAGS=$pgac_save_CPPFLAGS + LDFLAGS=$pgac_save_LDFLAGS + LIBS=$pgac_save_LIBS + fi if test "$with_gssapi" = yes ; then @@ -14516,6 +14537,13 @@ done fi +if test "$with_libcurl" = yes ; then + # Error out early if this platform can't support libpq-oauth. + if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then + as_fn_error $? "client OAuth is not supported on this platform" "$LINENO" 5 + fi +fi + ## ## Types, structures, compiler characteristics ## diff --git a/configure.ac b/configure.ac index 2a78cddd825..5d90bf0e979 100644 --- a/configure.ac +++ b/configure.ac @@ -1033,19 +1033,27 @@ if test "$with_libcurl" = yes ; then # to explicitly set TLS 1.3 ciphersuites). PKG_CHECK_MODULES(LIBCURL, [libcurl >= 7.61.0]) - # We only care about -I, -D, and -L switches; - # note that -lcurl will be added by PGAC_CHECK_LIBCURL below. + # Curl's flags are kept separate from the standard CPPFLAGS/LDFLAGS. We use + # them only for libpq-oauth. + LIBCURL_CPPFLAGS= + LIBCURL_LDFLAGS= + + # We only care about -I, -D, and -L switches. Note that -lcurl will be added + # to LIBCURL_LDLIBS by PGAC_CHECK_LIBCURL, below. for pgac_option in $LIBCURL_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) LIBCURL_CPPFLAGS="$LIBCURL_CPPFLAGS $pgac_option";; esac done for pgac_option in $LIBCURL_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBCURL_LDFLAGS="$LIBCURL_LDFLAGS $pgac_option";; esac done + AC_SUBST(LIBCURL_CPPFLAGS) + AC_SUBST(LIBCURL_LDFLAGS) + # OAuth requires python for testing if test "$with_python" != yes; then AC_MSG_WARN([*** OAuth support tests require --with-python to run]) @@ -1354,9 +1362,6 @@ failure. It is possible the compiler isn't looking in the proper directory. Use --without-zlib to disable zlib support.])]) fi -# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults -# during gss_acquire_cred(). This is possibly related to Curl's Heimdal -# dependency on that platform? if test "$with_libcurl" = yes ; then PGAC_CHECK_LIBCURL fi @@ -1654,6 +1659,13 @@ if test "$PORTNAME" = "win32" ; then AC_CHECK_HEADERS(crtdefs.h) fi +if test "$with_libcurl" = yes ; then + # Error out early if this platform can't support libpq-oauth. + if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then + AC_MSG_ERROR([client OAuth is not supported on this platform]) + fi +fi + ## ## Types, structures, compiler characteristics ## diff --git a/doc/src/sgml/installation.sgml b/doc/src/sgml/installation.sgml index 077bcc20759..d928b103d22 100644 --- a/doc/src/sgml/installation.sgml +++ b/doc/src/sgml/installation.sgml @@ -313,6 +313,14 @@ </para> </listitem> + <listitem> + <para> + You need <productname>Curl</productname> to build an optional module + which implements the <link linkend="libpq-oauth">OAuth Device + Authorization flow</link> for client applications. + </para> + </listitem> + <listitem> <para> You need <productname>LZ4</productname>, if you want to support diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 37102c235b0..ae536b2da07 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -10226,15 +10226,20 @@ void PQinitSSL(int do_ssl); <title>OAuth Support</title> <para> - libpq implements support for the OAuth v2 Device Authorization client flow, + <application>libpq</application> implements support for the OAuth v2 Device Authorization client flow, documented in <ulink url="https://datatracker.ietf.org/doc/html/rfc8628">RFC 8628</ulink>, - which it will attempt to use by default if the server + as an optional module. See the <link linkend="configure-option-with-libcurl"> + installation documentation</link> for information on how to enable support + for Device Authorization as a builtin flow. + </para> + <para> + When support is enabled and the optional module installed, <application>libpq</application> + will use the builtin flow by default if the server <link linkend="auth-oauth">requests a bearer token</link> during authentication. This flow can be utilized even if the system running the client application does not have a usable web browser, for example when - running a client via <application>SSH</application>. Client applications may implement their own flows - instead; see <xref linkend="libpq-oauth-authdata-hooks"/>. + running a client via <acronym>SSH</acronym>. </para> <para> The builtin flow will, by default, print a URL to visit and a user code to @@ -10251,6 +10256,11 @@ Visit https://example.com/device and enter the code: ABCD-EFGH they match expectations, before continuing. Permissions should not be given to untrusted third parties. </para> + <para> + Client applications may implement their own flows to customize interaction + and integration with applications. See <xref linkend="libpq-oauth-authdata-hooks"/> + for more information on how add a custom flow to <application>libpq</application>. + </para> <para> For an OAuth client flow to be usable, the connection string must at minimum contain <xref linkend="libpq-connect-oauth-issuer"/> and @@ -10366,7 +10376,9 @@ typedef struct _PGpromptOAuthDevice </synopsis> </para> <para> - The OAuth Device Authorization flow included in <application>libpq</application> + The OAuth Device Authorization flow which + <link linkend="configure-option-with-libcurl">can be included</link> + in <application>libpq</application> requires the end user to visit a URL with a browser, then enter a code which permits <application>libpq</application> to connect to the server on their behalf. The default prompt simply prints the @@ -10378,7 +10390,8 @@ typedef struct _PGpromptOAuthDevice This callback is only invoked during the builtin device authorization flow. If the application installs a <link linkend="libpq-oauth-authdata-oauth-bearer-token">custom OAuth - flow</link>, this authdata type will not be used. + flow</link>, or <application>libpq</application> was not built with + support for the builtin flow, this authdata type will not be used. </para> <para> If a non-NULL <structfield>verification_uri_complete</structfield> is @@ -10400,8 +10413,9 @@ typedef struct _PGpromptOAuthDevice </term> <listitem> <para> - Replaces the entire OAuth flow with a custom implementation. The hook - should either directly return a Bearer token for the current + Adds a custom implementation of a flow, replacing the builtin flow if + it is <link linkend="configure-option-with-libcurl">installed</link>. + The hook should either directly return a Bearer token for the current user/issuer/scope combination, if one is available without blocking, or else set up an asynchronous callback to retrieve one. </para> diff --git a/meson.build b/meson.build index a1516e54529..b902d4a00f8 100644 --- a/meson.build +++ b/meson.build @@ -107,6 +107,7 @@ os_deps = [] backend_both_deps = [] backend_deps = [] libpq_deps = [] +libpq_oauth_deps = [] pg_sysroot = '' @@ -860,13 +861,13 @@ endif ############################################################### libcurlopt = get_option('libcurl') +oauth_flow_supported = false + if not libcurlopt.disabled() # Check for libcurl 7.61.0 or higher (corresponding to RHEL8 and the ability # to explicitly set TLS 1.3 ciphersuites). libcurl = dependency('libcurl', version: '>= 7.61.0', required: libcurlopt) if libcurl.found() - cdata.set('USE_LIBCURL', 1) - # Check to see whether the current platform supports thread-safe Curl # initialization. libcurl_threadsafe_init = false @@ -938,6 +939,22 @@ if not libcurlopt.disabled() endif endif + # Check that the current platform supports our builtin flow. This requires + # libcurl and one of either epoll or kqueue. + oauth_flow_supported = ( + libcurl.found() + and (cc.check_header('sys/event.h', required: false, + args: test_c_args, include_directories: postgres_inc) + or cc.check_header('sys/epoll.h', required: false, + args: test_c_args, include_directories: postgres_inc)) + ) + + if oauth_flow_supported + cdata.set('USE_LIBCURL', 1) + elif libcurlopt.enabled() + error('client OAuth is not supported on this platform') + endif + else libcurl = not_found_dep endif @@ -3272,17 +3289,18 @@ libpq_deps += [ gssapi, ldap_r, - # XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults - # during gss_acquire_cred(). This is possibly related to Curl's Heimdal - # dependency on that platform? - libcurl, libintl, ssl, ] +libpq_oauth_deps += [ + libcurl, +] + subdir('src/interfaces/libpq') -# fe_utils depends on libpq +# fe_utils and libpq-oauth depends on libpq subdir('src/fe_utils') +subdir('src/interfaces/libpq-oauth') # for frontend binaries frontend_code = declare_dependency( diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 6722fbdf365..04952b533de 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -347,6 +347,9 @@ perl_embed_ldflags = @perl_embed_ldflags@ AWK = @AWK@ LN_S = @LN_S@ +LIBCURL_CPPFLAGS = @LIBCURL_CPPFLAGS@ +LIBCURL_LDFLAGS = @LIBCURL_LDFLAGS@ +LIBCURL_LDLIBS = @LIBCURL_LDLIBS@ MSGFMT = @MSGFMT@ MSGFMT_FLAGS = @MSGFMT_FLAGS@ MSGMERGE = @MSGMERGE@ diff --git a/src/interfaces/Makefile b/src/interfaces/Makefile index 7d56b29d28f..e6822caa206 100644 --- a/src/interfaces/Makefile +++ b/src/interfaces/Makefile @@ -14,7 +14,19 @@ include $(top_builddir)/src/Makefile.global SUBDIRS = libpq ecpg +ifeq ($(with_libcurl), yes) +SUBDIRS += libpq-oauth +else +ALWAYS_SUBDIRS += libpq-oauth +endif + $(recurse) +$(recurse_always) all-ecpg-recurse: all-libpq-recurse install-ecpg-recurse: install-libpq-recurse + +ifeq ($(with_libcurl), yes) +all-libpq-oauth-recurse: all-libpq-recurse +install-libpq-oauth-recurse: install-libpq-recurse +endif diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile new file mode 100644 index 00000000000..5fd251a1d27 --- /dev/null +++ b/src/interfaces/libpq-oauth/Makefile @@ -0,0 +1,65 @@ +#------------------------------------------------------------------------- +# +# Makefile for libpq-oauth +# +# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1994, Regents of the University of California +# +# src/interfaces/libpq-oauth/Makefile +# +#------------------------------------------------------------------------- + +subdir = src/interfaces/libpq-oauth +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +PGFILEDESC = "libpq-oauth - device authorization OAuth support" + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. +NAME = pq-oauth-$(MAJORVERSION) + +# Force the name "libpq-oauth" for both the static and shared libraries. +override shlib := lib$(NAME)$(DLSUFFIX) +override stlib := lib$(NAME).a + +override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(LIBCURL_CPPFLAGS) $(CPPFLAGS) + +OBJS = \ + $(WIN32RES) \ + oauth-curl.o + +# The shared library needs additional glue symbols. +$(shlib): OBJS += oauth-utils.o +$(shlib): oauth-utils.o + +SHLIB_LINK_INTERNAL = $(libpq_pgport_shlib) +SHLIB_LINK = $(LIBCURL_LDFLAGS) $(LIBCURL_LDLIBS) +SHLIB_PREREQS = submake-libpq +SHLIB_EXPORTS = exports.txt + +# Disable -bundle_loader on macOS. +BE_DLLLIBS = + +# By default, a library without an SONAME doesn't get a static library, so we +# add it to the build explicitly. +all: all-lib all-static-lib + +# Shared library stuff +include $(top_srcdir)/src/Makefile.shlib + +# Ignore the standard rules for SONAME-less installation; we want both the +# static and shared libraries to go into libdir. +install: all installdirs $(stlib) $(shlib) + $(INSTALL_SHLIB) $(shlib) '$(DESTDIR)$(libdir)/$(shlib)' + $(INSTALL_STLIB) $(stlib) '$(DESTDIR)$(libdir)/$(stlib)' + +installdirs: + $(MKDIR_P) '$(DESTDIR)$(libdir)' + +uninstall: + rm -f '$(DESTDIR)$(libdir)/$(stlib)' + rm -f '$(DESTDIR)$(libdir)/$(shlib)' + +clean distclean: clean-lib + rm -f $(OBJS) oauth-utils.o diff --git a/src/interfaces/libpq-oauth/README b/src/interfaces/libpq-oauth/README new file mode 100644 index 00000000000..45def6c1ab6 --- /dev/null +++ b/src/interfaces/libpq-oauth/README @@ -0,0 +1,43 @@ +libpq-oauth is an optional module implementing the Device Authorization flow for +OAuth clients (RFC 8628). It was originally developed as part of libpq core and +later split out as its own shared library in order to isolate its dependency on +libcurl. (End users who don't want the Curl dependency can simply choose not to +install this module.) + +If a connection string allows the use of OAuth, and the server asks for it, and +a libpq client has not installed its own custom OAuth flow, libpq will attempt +to delay-load this module using dlopen() and the following ABI. Failure to load +results in a failed connection. + += Load-Time ABI = + +This module ABI is an internal implementation detail, so it's subject to change +across major releases; the name of the module (libpq-oauth-MAJOR) reflects this. +The module exports the following symbols: + +- PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +- void pg_fe_cleanup_oauth_flow(PGconn *conn); + +pg_fe_run_oauth_flow and pg_fe_cleanup_oauth_flow are implementations of +conn->async_auth and conn->cleanup_async_auth, respectively. + +- void libpq_oauth_init(pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + +At the moment, pg_fe_run_oauth_flow() relies on libpq's pg_g_threadlock and +libpq_gettext(), which must be injected by libpq using this initialization +function before the flow is run. It also relies on libpq to expose +conn->errorMessage, via the errmsg_impl. + +This dependency injection is done to ensure that the module ABI is decoupled +from the internals of `struct pg_conn`. This way we can safely search the +standard dlopen() paths (e.g. RPATH, LD_LIBRARY_PATH, the SO cache) for an +implementation module to use, even if that module wasn't compiled at the same +time as libpq. + += Static Build = + +The static library libpq.a does not perform any dynamic loading. If the builtin +flow is enabled, the application is expected to link against libpq-oauth-*.a +directly to provide the necessary symbols. diff --git a/src/interfaces/libpq-oauth/exports.txt b/src/interfaces/libpq-oauth/exports.txt new file mode 100644 index 00000000000..6891a83dbf9 --- /dev/null +++ b/src/interfaces/libpq-oauth/exports.txt @@ -0,0 +1,4 @@ +# src/interfaces/libpq-oauth/exports.txt +libpq_oauth_init 1 +pg_fe_run_oauth_flow 2 +pg_fe_cleanup_oauth_flow 3 diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build new file mode 100644 index 00000000000..cf597e1da1e --- /dev/null +++ b/src/interfaces/libpq-oauth/meson.build @@ -0,0 +1,43 @@ +# Copyright (c) 2022-2025, PostgreSQL Global Development Group + +if not oauth_flow_supported + subdir_done() +endif + +libpq_oauth_sources = files( + 'oauth-curl.c', +) + +# The shared library needs additional glue symbols. +libpq_oauth_so_sources = files( + 'oauth-utils.c', +) + +export_file = custom_target('libpq-oauth.exports', + kwargs: gen_export_kwargs, +) + +# port needs to be in include path due to pthread-win32.h +libpq_oauth_inc = include_directories('.', '../libpq', '../../port') + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. +libpq_oauth_name = 'libpq-oauth-@0@'.format(pg_version_major) + +libpq_oauth_st = static_library(libpq_oauth_name, + libpq_oauth_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_pch: pch_postgres_fe_h, + dependencies: [frontend_stlib_code, libpq_oauth_deps], + kwargs: default_lib_args, +) + +libpq_oauth_so = shared_module(libpq_oauth_name, + libpq_oauth_sources + libpq_oauth_so_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_pch: pch_postgres_fe_h, + dependencies: [frontend_shlib_code, libpq, libpq_oauth_deps], + link_depends: export_file, + link_args: export_fmt.format(export_file.full_path()), + kwargs: default_lib_args, +) diff --git a/src/interfaces/libpq/fe-auth-oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c similarity index 98% rename from src/interfaces/libpq/fe-auth-oauth-curl.c rename to src/interfaces/libpq-oauth/oauth-curl.c index cd9c0323bb6..d52125415bc 100644 --- a/src/interfaces/libpq/fe-auth-oauth-curl.c +++ b/src/interfaces/libpq-oauth/oauth-curl.c @@ -1,6 +1,6 @@ /*------------------------------------------------------------------------- * - * fe-auth-oauth-curl.c + * oauth-curl.c * The libcurl implementation of OAuth/OIDC authentication, using the * OAuth Device Authorization Grant (RFC 8628). * @@ -8,7 +8,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * src/interfaces/libpq/fe-auth-oauth-curl.c + * src/interfaces/libpq-oauth/oauth-curl.c * *------------------------------------------------------------------------- */ @@ -17,20 +17,23 @@ #include <curl/curl.h> #include <math.h> -#ifdef HAVE_SYS_EPOLL_H +#include <unistd.h> + +#if defined(HAVE_SYS_EPOLL_H) #include <sys/epoll.h> #include <sys/timerfd.h> -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) #include <sys/event.h> +#else +#error libpq-oauth is not supported on this platform #endif -#include <unistd.h> #include "common/jsonapi.h" #include "fe-auth.h" #include "fe-auth-oauth.h" -#include "libpq-int.h" #include "mb/pg_wchar.h" +#include "oauth-curl.h" +#include "oauth-utils.h" /* * It's generally prudent to set a maximum response size to buffer in memory, @@ -1110,7 +1113,7 @@ parse_access_token(struct async_ctx *actx, struct token *tok) static bool setup_multiplexer(struct async_ctx *actx) { -#ifdef HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct epoll_event ev = {.events = EPOLLIN}; actx->mux = epoll_create1(EPOLL_CLOEXEC); @@ -1134,8 +1137,7 @@ setup_multiplexer(struct async_ctx *actx) } return true; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) actx->mux = kqueue(); if (actx->mux < 0) { @@ -1158,10 +1160,9 @@ setup_multiplexer(struct async_ctx *actx) } return true; +#else +#error setup_multiplexer is not implemented on this platform #endif - - actx_error(actx, "libpq does not support the Device Authorization flow on this platform"); - return false; } /* @@ -1174,7 +1175,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, { struct async_ctx *actx = ctx; -#ifdef HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct epoll_event ev = {0}; int res; int op = EPOLL_CTL_ADD; @@ -1230,8 +1231,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, } return 0; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) struct kevent ev[2] = {0}; struct kevent ev_out[2]; struct timespec timeout = {0}; @@ -1312,10 +1312,9 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, } return 0; +#else +#error register_socket is not implemented on this platform #endif - - actx_error(actx, "libpq does not support multiplexer sockets on this platform"); - return -1; } /* @@ -1334,7 +1333,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, static bool set_timer(struct async_ctx *actx, long timeout) { -#if HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct itimerspec spec = {0}; if (timeout < 0) @@ -1363,8 +1362,7 @@ set_timer(struct async_ctx *actx, long timeout) } return true; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) struct kevent ev; #ifdef __NetBSD__ @@ -1419,10 +1417,9 @@ set_timer(struct async_ctx *actx, long timeout) } return true; +#else +#error set_timer is not implemented on this platform #endif - - actx_error(actx, "libpq does not support timers on this platform"); - return false; } /* @@ -1433,7 +1430,7 @@ set_timer(struct async_ctx *actx, long timeout) static int timer_expired(struct async_ctx *actx) { -#if HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct itimerspec spec = {0}; if (timerfd_gettime(actx->timerfd, &spec) < 0) @@ -1453,8 +1450,7 @@ timer_expired(struct async_ctx *actx) /* If the remaining time to expiration is zero, we're done. */ return (spec.it_value.tv_sec == 0 && spec.it_value.tv_nsec == 0); -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) int res; /* Is the timer queue ready? */ @@ -1466,10 +1462,9 @@ timer_expired(struct async_ctx *actx) } return (res > 0); +#else +#error timer_expired is not implemented on this platform #endif - - actx_error(actx, "libpq does not support timers on this platform"); - return -1; } /* @@ -2487,8 +2482,9 @@ prompt_user(struct async_ctx *actx, PGconn *conn) .verification_uri_complete = actx->authz.verification_uri_complete, .expires_in = actx->authz.expires_in, }; + PQauthDataHook_type hook = PQgetAuthDataHook(); - res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); + res = hook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); if (!res) { diff --git a/src/interfaces/libpq-oauth/oauth-curl.h b/src/interfaces/libpq-oauth/oauth-curl.h new file mode 100644 index 00000000000..248d0424ad0 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-curl.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * oauth-curl.h + * + * Definitions for OAuth Device Authorization module + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-curl.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_CURL_H +#define OAUTH_CURL_H + +#include "libpq-fe.h" + +/* Exported async-auth callbacks. */ +extern PGDLLEXPORT PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +extern PGDLLEXPORT void pg_fe_cleanup_oauth_flow(PGconn *conn); + +#endif /* OAUTH_CURL_H */ diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c new file mode 100644 index 00000000000..62ba4299ec1 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.c @@ -0,0 +1,198 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.c + * + * "Glue" helpers providing a copy of some internal APIs from libpq. At + * some point in the future, we might be able to deduplicate. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq-oauth/oauth-utils.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include <signal.h> + +#include "libpq-int.h" +#include "oauth-utils.h" + +static libpq_gettext_func libpq_gettext_impl; +static conn_errorMessage_func conn_errorMessage; + +pgthreadlock_t pg_g_threadlock; + +/*- + * Initializes libpq-oauth by setting necessary callbacks. + * + * The current implementation relies on the following private implementation + * details of libpq: + * + * - pg_g_threadlock: protects libcurl initialization if the underlying Curl + * installation is not threadsafe + * + * - libpq_gettext: translates error messages using libpq's message domain + * + * - conn->errorMessage: holds translated errors for the connection. This is + * handled through a translation shim, which avoids either depending on the + * offset of the errorMessage in PGconn, or needing to export the variadic + * libpq_append_conn_error(). + */ +void +libpq_oauth_init(pgthreadlock_t threadlock_impl, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl) +{ + pg_g_threadlock = threadlock_impl; + libpq_gettext_impl = gettext_impl; + conn_errorMessage = errmsg_impl; +} + +/* + * Append a formatted string to the error message buffer of the given + * connection, after translating it. This is a copy of libpq's internal API. + */ +void +libpq_append_conn_error(PGconn *conn, const char *fmt,...) +{ + int save_errno = errno; + bool done; + va_list args; + PQExpBuffer errorMessage = conn_errorMessage(conn); + + Assert(fmt[strlen(fmt) - 1] != '\n'); + + if (PQExpBufferBroken(errorMessage)) + return; /* already failed */ + + /* Loop in case we have to retry after enlarging the buffer. */ + do + { + errno = save_errno; + va_start(args, fmt); + done = appendPQExpBufferVA(errorMessage, libpq_gettext(fmt), args); + va_end(args); + } while (!done); + + appendPQExpBufferChar(errorMessage, '\n'); +} + +#ifdef ENABLE_NLS + +/* + * A shim that defers to the actual libpq_gettext(). + */ +char * +libpq_gettext(const char *msgid) +{ + if (!libpq_gettext_impl) + { + /* + * Possible if the libpq build didn't enable NLS but the libpq-oauth + * build did. That's an odd mismatch, but we can handle it. + * + * Note that callers of libpq_gettext() have to treat the return value + * as if it were const, because builds without NLS simply pass through + * their argument. + */ + return unconstify(char *, msgid); + } + + return libpq_gettext_impl(msgid); +} + +#endif /* ENABLE_NLS */ + +/* + * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment. + */ +bool +oauth_unsafe_debugging_enabled(void) +{ + const char *env = getenv("PGOAUTHDEBUG"); + + return (env && strcmp(env, "UNSAFE") == 0); +} + +/* + * Duplicate SOCK_ERRNO* definitions from libpq-int.h, for use by + * pq_block/reset_sigpipe(). + */ +#ifdef WIN32 +#define SOCK_ERRNO (WSAGetLastError()) +#define SOCK_ERRNO_SET(e) WSASetLastError(e) +#else +#define SOCK_ERRNO errno +#define SOCK_ERRNO_SET(e) (errno = (e)) +#endif + +/* + * Block SIGPIPE for this thread. This is a copy of libpq's internal API. + */ +int +pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending) +{ + sigset_t sigpipe_sigset; + sigset_t sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + /* Block SIGPIPE and save previous mask for later reset */ + SOCK_ERRNO_SET(pthread_sigmask(SIG_BLOCK, &sigpipe_sigset, osigset)); + if (SOCK_ERRNO) + return -1; + + /* We can have a pending SIGPIPE only if it was blocked before */ + if (sigismember(osigset, SIGPIPE)) + { + /* Is there a pending SIGPIPE? */ + if (sigpending(&sigset) != 0) + return -1; + + if (sigismember(&sigset, SIGPIPE)) + *sigpipe_pending = true; + else + *sigpipe_pending = false; + } + else + *sigpipe_pending = false; + + return 0; +} + +/* + * Discard any pending SIGPIPE and reset the signal mask. This is a copy of + * libpq's internal API. + */ +void +pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe) +{ + int save_errno = SOCK_ERRNO; + int signo; + sigset_t sigset; + + /* Clear SIGPIPE only if none was pending */ + if (got_epipe && !sigpipe_pending) + { + if (sigpending(&sigset) == 0 && + sigismember(&sigset, SIGPIPE)) + { + sigset_t sigpipe_sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + sigwait(&sigpipe_sigset, &signo); + } + } + + /* Restore saved block mask */ + pthread_sigmask(SIG_SETMASK, osigset, NULL); + + SOCK_ERRNO_SET(save_errno); +} diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h new file mode 100644 index 00000000000..279fc113248 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.h @@ -0,0 +1,35 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.h + * + * Definitions providing missing libpq internal APIs + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-utils.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_UTILS_H +#define OAUTH_UTILS_H + +#include "libpq-fe.h" +#include "pqexpbuffer.h" + +typedef char *(*libpq_gettext_func) (const char *msgid); +typedef PQExpBuffer (*conn_errorMessage_func) (PGconn *conn); + +/* Initializes libpq-oauth. */ +extern PGDLLEXPORT void libpq_oauth_init(pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + +/* Duplicated APIs, copied from libpq. */ +extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3); +extern bool oauth_unsafe_debugging_enabled(void); +extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending); +extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe); + +#endif /* OAUTH_UTILS_H */ diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 90b0b65db6f..d4c20066ce4 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -31,7 +31,6 @@ endif OBJS = \ $(WIN32RES) \ - fe-auth-oauth.o \ fe-auth-scram.o \ fe-cancel.o \ fe-connect.o \ @@ -64,9 +63,11 @@ OBJS += \ fe-secure-gssapi.o endif -ifeq ($(with_libcurl),yes) -OBJS += fe-auth-oauth-curl.o -endif +# The OAuth implementation differs depending on the type of library being built. +OBJS_STATIC = fe-auth-oauth.o + +fe-auth-oauth_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH +OBJS_SHLIB = fe-auth-oauth_shlib.o ifeq ($(PORTNAME), cygwin) override shlib = cyg$(NAME)$(DLSUFFIX) @@ -86,7 +87,7 @@ endif # that are built correctly for use in a shlib. SHLIB_LINK_INTERNAL = -lpgcommon_shlib -lpgport_shlib ifneq ($(PORTNAME), win32) -SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lcurl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) +SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) else SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi32 -lssl -lsocket -lnsl -lresolv -lintl -lm $(PTHREAD_LIBS), $(LIBS)) $(LDAP_LIBS_FE) endif @@ -101,12 +102,26 @@ ifeq ($(with_ssl),openssl) PKG_CONFIG_REQUIRES_PRIVATE = libssl, libcrypto endif +ifeq ($(with_libcurl),yes) +# libpq.so doesn't link against libcurl, but libpq.a needs libpq-oauth, and +# libpq-oauth needs libcurl. Put both into *.private. +PKG_CONFIG_REQUIRES_PRIVATE += libcurl +%.pc: override SHLIB_LINK_INTERNAL += -lpq-oauth-$(MAJORVERSION) +endif + all: all-lib libpq-refs-stamp # Shared library stuff include $(top_srcdir)/src/Makefile.shlib backend_src = $(top_srcdir)/src/backend +# Add shlib-/stlib-specific objects. +$(shlib): override OBJS += $(OBJS_SHLIB) +$(shlib): $(OBJS_SHLIB) + +$(stlib): override OBJS += $(OBJS_STATIC) +$(stlib): $(OBJS_STATIC) + # Check for functions that libpq must not call, currently just exit(). # (Ideally we'd reject abort() too, but there are various scenarios where # build toolchains insert abort() calls, e.g. to implement assert().) @@ -115,8 +130,6 @@ backend_src = $(top_srcdir)/src/backend # which seems to insert references to that even in pure C code. Excluding # __tsan_func_exit is necessary when using ThreadSanitizer data race detector # which use this function for instrumentation of function exit. -# libcurl registers an exit handler in the memory debugging code when running -# with LeakSanitizer. # Skip the test when profiling, as gcc may insert exit() calls for that. # Also skip the test on platforms where libpq infrastructure may be provided # by statically-linked libraries, as we can't expect them to honor this @@ -124,7 +137,7 @@ backend_src = $(top_srcdir)/src/backend libpq-refs-stamp: $(shlib) ifneq ($(enable_coverage), yes) ifeq (,$(filter solaris,$(PORTNAME))) - @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit -e _atexit | grep exit; then \ + @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit | grep exit; then \ echo 'libpq must not be calling any function which invokes exit'; exit 1; \ fi endif @@ -138,6 +151,11 @@ fe-misc.o: fe-misc.c $(top_builddir)/src/port/pg_config_paths.h $(top_builddir)/src/port/pg_config_paths.h: $(MAKE) -C $(top_builddir)/src/port pg_config_paths.h +# Use src/common/Makefile's trick for tracking dependencies of shlib-specific +# objects. +%_shlib.o: %.c %.o + $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@ + install: all installdirs install-lib $(INSTALL_DATA) $(srcdir)/libpq-fe.h '$(DESTDIR)$(includedir)' $(INSTALL_DATA) $(srcdir)/libpq-events.h '$(DESTDIR)$(includedir)' @@ -171,6 +189,6 @@ uninstall: uninstall-lib clean distclean: clean-lib $(MAKE) -C test $@ rm -rf tmp_check - rm -f $(OBJS) pthread.h libpq-refs-stamp + rm -f $(OBJS) $(OBJS_SHLIB) $(OBJS_STATIC) pthread.h libpq-refs-stamp # Might be left over from a Win32 client-only build rm -f pg_config_paths.h diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index d5143766858..0625cf39e9a 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -210,3 +210,4 @@ PQsetAuthDataHook 207 PQgetAuthDataHook 208 PQdefaultAuthDataHook 209 PQfullProtocolVersion 210 +appendPQExpBufferVA 211 diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c index cf1a25e2ccc..20c848ec9e0 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,186 @@ 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); +typedef PQExpBuffer (*conn_errorMessage_func) (PGconn *conn); + +/* + * This shim is injected into libpq-oauth so that it doesn't depend on the + * offset of conn->errorMessage. + * + * TODO: look into exporting libpq_append_conn_error or a comparable API from + * libpq, instead. + */ +static PQExpBuffer +conn_errorMessage(PGconn *conn) +{ + return &conn->errorMessage; +} + +/* + * 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); + 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); + + 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 +977,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 custom OAuth flows are available, and the builtin flow is not installed"); goto fail; - -#endif } return true; diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h index 3f1a7503a01..687e664475f 100644 --- a/src/interfaces/libpq/fe-auth-oauth.h +++ b/src/interfaces/libpq/fe-auth-oauth.h @@ -33,12 +33,13 @@ typedef struct PGconn *conn; void *async_ctx; + + void *builtin_flow; } fe_oauth_state; -extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); -extern void pg_fe_cleanup_oauth_flow(PGconn *conn); extern void pqClearOAuthToken(PGconn *conn); extern bool oauth_unsafe_debugging_enabled(void); +extern bool use_builtin_flow(PGconn *conn, fe_oauth_state *state); /* Mechanisms in fe-auth-oauth.c */ extern const pg_fe_sasl_mech pg_oauth_mech; diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build index 292fecf3320..29b3451e3aa 100644 --- a/src/interfaces/libpq/meson.build +++ b/src/interfaces/libpq/meson.build @@ -38,10 +38,6 @@ if gssapi.found() ) endif -if libcurl.found() - libpq_sources += files('fe-auth-oauth-curl.c') -endif - export_file = custom_target('libpq.exports', kwargs: gen_export_kwargs, ) @@ -50,6 +46,9 @@ export_file = custom_target('libpq.exports', libpq_inc = include_directories('.', '../../port') libpq_c_args = ['-DSO_MAJOR_VERSION=5'] +# The OAuth implementation differs depending on the type of library being built. +libpq_so_c_args = ['-DUSE_DYNAMIC_OAUTH'] + # Not using both_libraries() here as # 1) resource files should only be in the shared library # 2) we want the .pc file to include a dependency to {pgport,common}_static for @@ -70,7 +69,7 @@ libpq_st = static_library('libpq', libpq_so = shared_library('libpq', libpq_sources + libpq_so_sources, include_directories: [libpq_inc, postgres_inc], - c_args: libpq_c_args, + c_args: libpq_c_args + libpq_so_c_args, c_pch: pch_postgres_fe_h, version: '5.' + pg_version_major.to_string(), soversion: host_system != 'windows' ? '5' : '', @@ -86,12 +85,26 @@ libpq = declare_dependency( include_directories: [include_directories('.')] ) +private_deps = [ + frontend_stlib_code, + libpq_deps, +] + +if oauth_flow_supported + # libpq.so doesn't link against libcurl, but libpq.a needs libpq-oauth, and + # libpq-oauth needs libcurl. Put both into *.private. + private_deps += [ + libpq_oauth_deps, + '-lpq-oauth-@0@'.format(pg_version_major), + ] +endif + pkgconfig.generate( name: 'libpq', description: 'PostgreSQL libpq library', url: pg_url, libraries: libpq, - libraries_private: [frontend_stlib_code, libpq_deps], + libraries_private: private_deps, ) install_headers( diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk index ae761265852..b87df277d93 100644 --- a/src/interfaces/libpq/nls.mk +++ b/src/interfaces/libpq/nls.mk @@ -13,15 +13,21 @@ GETTEXT_FILES = fe-auth.c \ fe-secure-common.c \ fe-secure-gssapi.c \ fe-secure-openssl.c \ - win32.c -GETTEXT_TRIGGERS = libpq_append_conn_error:2 \ + win32.c \ + ../libpq-oauth/oauth-curl.c \ + ../libpq-oauth/oauth-utils.c +GETTEXT_TRIGGERS = actx_error:2 \ + libpq_append_conn_error:2 \ libpq_append_error:2 \ libpq_gettext \ libpq_ngettext:1,2 \ + oauth_parse_set_error:2 \ pqInternalNotice:2 -GETTEXT_FLAGS = libpq_append_conn_error:2:c-format \ +GETTEXT_FLAGS = actx_error:2:c-format \ + libpq_append_conn_error:2:c-format \ libpq_append_error:2:c-format \ libpq_gettext:1:pass-c-format \ libpq_ngettext:1:pass-c-format \ libpq_ngettext:2:pass-c-format \ + oauth_parse_set_error:2:c-format \ pqInternalNotice:2:c-format diff --git a/src/makefiles/meson.build b/src/makefiles/meson.build index 55da678ec27..91a8de1ee9b 100644 --- a/src/makefiles/meson.build +++ b/src/makefiles/meson.build @@ -203,6 +203,8 @@ pgxs_empty = [ 'LIBNUMA_CFLAGS', 'LIBNUMA_LIBS', 'LIBURING_CFLAGS', 'LIBURING_LIBS', + + 'LIBCURL_CPPFLAGS', 'LIBCURL_LDFLAGS', 'LIBCURL_LDLIBS', ] if host_system == 'windows' and cc.get_argument_syntax() != 'msvc' diff --git a/src/test/modules/oauth_validator/meson.build b/src/test/modules/oauth_validator/meson.build index 36d1b26369f..e190f9cf15a 100644 --- a/src/test/modules/oauth_validator/meson.build +++ b/src/test/modules/oauth_validator/meson.build @@ -78,7 +78,7 @@ tests += { ], 'env': { 'PYTHON': python.path(), - 'with_libcurl': libcurl.found() ? 'yes' : 'no', + 'with_libcurl': oauth_flow_supported ? 'yes' : 'no', 'with_python': 'yes', }, }, diff --git a/src/test/modules/oauth_validator/t/002_client.pl b/src/test/modules/oauth_validator/t/002_client.pl index 54769f12f57..c23b53ac98f 100644 --- a/src/test/modules/oauth_validator/t/002_client.pl +++ b/src/test/modules/oauth_validator/t/002_client.pl @@ -111,7 +111,7 @@ if ($ENV{with_libcurl} ne 'yes') "fails without custom hook installed", flags => ["--no-hook"], expected_stderr => - qr/no custom OAuth flows are available, and libpq was not built with libcurl support/ + qr/no custom OAuth flows are available, and the builtin flow is not installed/ ); } -- 2.34.1 ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-18 17:01 Jacob Champion <[email protected]> parent: Jacob Champion <[email protected]> 1 sibling, 0 replies; 36+ messages in thread From: Jacob Champion @ 2025-04-18 17:01 UTC (permalink / raw) To: Jelte Fennema-Nio <[email protected]>; +Cc: Peter Eisentraut <[email protected]>; Christoph Berg <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]> On Thu, Apr 17, 2025 at 5:47 PM Jacob Champion <[email protected]> wrote: > With those, I have no more TODOs and I believe this is ready for a > final review round. Some ABI self-review. These references to conn->errorMessage also need the indirection treatment, which I'm working on now: > if (actx->errctx) > { > appendPQExpBufferStr(&conn->errorMessage, > libpq_gettext(actx->errctx)); > appendPQExpBufferStr(&conn->errorMessage, ": "); > ... I was searching backwards through history to confirm that we don't rearrange struct pg_conn in back branches; turns out that was a false assumption. See e8f60e6fe2: While at it, fix some places where parameter-related infrastructure was added with the aid of a dartboard, or perhaps with the aid of the anti-pattern "add new stuff at the end". It should be safe to rearrange the contents of struct pg_conn even in released branches, since that's private to libpq (and we'd have to move some fields in some builds to fix this, anyway). So that means, I think, the name needs to go back to -<major>-<minor>, unless anyone can think of a clever way around it. (Injecting conn->errorMessage to avoid the messiness around ENABLE_GSS et al is still useful, but injecting every single offset doesn't seem maintainable to me.) Sorry, Christoph; I know that's not what you were hoping for. --Jacob ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-19 12:03 Christoph Berg <[email protected]> parent: Jacob Champion <[email protected]> 1 sibling, 1 reply; 36+ messages in thread From: Christoph Berg @ 2025-04-19 12:03 UTC (permalink / raw) To: Jacob Champion <[email protected]>; +Cc: Jelte Fennema-Nio <[email protected]>; Peter Eisentraut <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]>; Devrim Gündüz <[email protected]> Re: Jacob Champion > > libpq_append_conn_error(conn, "no custom OAuth flows are available, > > and libpq-oauth could not be loaded library could not be loaded. Try > > installing the libpq-oauth package from the same source that you > > installed libpq from"); > > Thanks! I think that's a little too prescriptive for packagers, > personally, but I agree that the current message isn't correct > anymore. I've gone with "no custom OAuth flows are available, and the > builtin flow is not installed". This whole oauth business is highly confusing if you aren't a web security expert. It's a pretty long way from "the builtin flow is not installed" to "if you want this to work, you need to install an extra library/package on the client", so I don't think this message is helpful. The originally suggested message was pretty good in that regard. The distinction about custom flows could probably be dropped. How about this: No libpq OAuth flows are available. (Try installing the libpq-oauth package.) People who have custom flows will likely know that they have to do anyway. Devrim: Does that match the package name you'd use? > (I suppose packagers could patch in a > platform-specific message if they really wanted?) We could, but I'd prefer if we didn't have to. :*) Christoph ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-21 23:19 Jacob Champion <[email protected]> parent: Christoph Berg <[email protected]> 0 siblings, 1 reply; 36+ messages in thread From: Jacob Champion @ 2025-04-21 23:19 UTC (permalink / raw) To: Christoph Berg <[email protected]>; Jacob Champion <[email protected]>; Jelte Fennema-Nio <[email protected]>; Peter Eisentraut <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Daniel Gustafsson <[email protected]>; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]>; Devrim Gündüz <[email protected]> On Sat, Apr 19, 2025 at 5:04 AM Christoph Berg <[email protected]> wrote: > How about this: > > No libpq OAuth flows are available. (Try installing the libpq-oauth package.) Tweaked for capitalization/punctuation rules, and removing the first "libpq" mention (which I don't think helps a user of, say, psql): no OAuth flows are available (try installing the libpq-oauth package) v8 also makes the following changes: - Per ABI comment upthread, we are back to major-minor versioning for the shared library (e.g. libpq-oauth-18-0.so). 0001 adds the macros and makefile variables to make this easy, and 0002 is the bulk of the change now. - Since libpq-oauth.a is going to be discovered at compile time, not runtime, I've removed the versioning from that filename. Static clients need to match them anyway, so we don't need that additional packaging headache. - conn->errorMessage is now decoupled from oauth-curl.c. Separate object file builds are made using the same technique as libpq. Thanks, --Jacob -: ----------- > 1: 5f87f11b18e Add minor-version counterpart to (PG_)MAJORVERSION 1: 942ad5391e2 ! 2: 4c9cc7f69af oauth: Move the builtin flow into a separate module @@ Commit message the search path came from a different build of Postgres. 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 major 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.) + symlinks, and it's named libpq-oauth-<major>-<minor>.so to avoid mixing + and matching across Postgres versions, in case internal struct order + needs to change. (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 @@ src/interfaces/libpq-oauth/Makefile (new) + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. -+NAME = pq-oauth-$(MAJORVERSION) ++NAME = pq-oauth-$(MAJORVERSION)-$(MINORVERSION) + -+# Force the name "libpq-oauth" for both the static and shared libraries. ++# Force the name "libpq-oauth" for both the static and shared libraries. The ++# staticlib doesn't need version information in its name. +override shlib := lib$(NAME)$(DLSUFFIX) -+override stlib := lib$(NAME).a ++override stlib := libpq-oauth.a + +override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(LIBCURL_CPPFLAGS) $(CPPFLAGS) + +OBJS = \ -+ $(WIN32RES) \ -+ oauth-curl.o ++ $(WIN32RES) ++ ++OBJS_STATIC = oauth-curl.o + +# The shared library needs additional glue symbols. -+$(shlib): OBJS += oauth-utils.o -+$(shlib): oauth-utils.o ++OBJS_SHLIB = \ ++ oauth-curl_shlib.o \ ++ oauth-utils.o \ ++ ++oauth-utils.o: override CPPFLAGS += -DUSE_DYNAMIC_OAUTH ++oauth-curl_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH ++ ++# Add shlib-/stlib-specific objects. ++$(shlib): override OBJS += $(OBJS_SHLIB) ++$(shlib): $(OBJS_SHLIB) ++ ++$(stlib): override OBJS += $(OBJS_STATIC) ++$(stlib): $(OBJS_STATIC) + +SHLIB_LINK_INTERNAL = $(libpq_pgport_shlib) +SHLIB_LINK = $(LIBCURL_LDFLAGS) $(LIBCURL_LDLIBS) @@ src/interfaces/libpq-oauth/Makefile (new) +# Shared library stuff +include $(top_srcdir)/src/Makefile.shlib + ++# Use src/common/Makefile's trick for tracking dependencies of shlib-specific ++# objects. ++%_shlib.o: %.c %.o ++ $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@ ++ +# Ignore the standard rules for SONAME-less installation; we want both the +# static and shared libraries to go into libdir. +install: all installdirs $(stlib) $(shlib) @@ src/interfaces/libpq-oauth/Makefile (new) + rm -f '$(DESTDIR)$(libdir)/$(shlib)' + +clean distclean: clean-lib -+ rm -f $(OBJS) oauth-utils.o ++ rm -f $(OBJS) $(OBJS_STATIC) $(OBJS_SHLIB) ## src/interfaces/libpq-oauth/README (new) ## @@ @@ src/interfaces/libpq-oauth/README (new) += Load-Time ABI = + +This module ABI is an internal implementation detail, so it's subject to change -+across major releases; the name of the module (libpq-oauth-MAJOR) reflects this. ++across releases; the name of the module (libpq-oauth-MAJOR-MINOR) reflects this. +The module exports the following symbols: + +- PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); @@ src/interfaces/libpq-oauth/README (new) + +At the moment, pg_fe_run_oauth_flow() relies on libpq's pg_g_threadlock and +libpq_gettext(), which must be injected by libpq using this initialization -+function before the flow is run. It also relies on libpq to expose -+conn->errorMessage, via the errmsg_impl. ++function before the flow is run. + -+This dependency injection is done to ensure that the module ABI is decoupled -+from the internals of `struct pg_conn`. This way we can safely search the -+standard dlopen() paths (e.g. RPATH, LD_LIBRARY_PATH, the SO cache) for an -+implementation module to use, even if that module wasn't compiled at the same -+time as libpq. ++It also relies on libpq to expose conn->errorMessage, via the errmsg_impl. This ++is done to decouple the module ABI from the offset of errorMessage, which can ++change positions depending on configure-time options. This way we can safely ++search the standard dlopen() paths (e.g. RPATH, LD_LIBRARY_PATH, the SO cache) ++for an implementation module to use, even if that module wasn't compiled at the ++same time as libpq. + += Static Build = + +The static library libpq.a does not perform any dynamic loading. If the builtin -+flow is enabled, the application is expected to link against libpq-oauth-*.a ++flow is enabled, the application is expected to link against libpq-oauth.a +directly to provide the necessary symbols. ## src/interfaces/libpq-oauth/exports.txt (new) ## @@ src/interfaces/libpq-oauth/meson.build (new) +libpq_oauth_so_sources = files( + 'oauth-utils.c', +) ++libpq_oauth_so_c_args = ['-DUSE_DYNAMIC_OAUTH'] + +export_file = custom_target('libpq-oauth.exports', + kwargs: gen_export_kwargs, @@ src/interfaces/libpq-oauth/meson.build (new) +# port needs to be in include path due to pthread-win32.h +libpq_oauth_inc = include_directories('.', '../libpq', '../../port') + -+# This is an internal module; we don't want an SONAME and therefore do not set -+# SO_MAJOR_VERSION. -+libpq_oauth_name = 'libpq-oauth-@0@'.format(pg_version_major) -+ -+libpq_oauth_st = static_library(libpq_oauth_name, ++libpq_oauth_st = static_library('libpq-oauth', + libpq_oauth_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_pch: pch_postgres_fe_h, @@ src/interfaces/libpq-oauth/meson.build (new) + kwargs: default_lib_args, +) + ++# This is an internal module; we don't want an SONAME and therefore do not set ++# SO_MAJOR_VERSION. ++libpq_oauth_name = 'libpq-oauth-@0@-@1@'.format(pg_version_major, pg_version_minor) ++ +libpq_oauth_so = shared_module(libpq_oauth_name, + libpq_oauth_sources + libpq_oauth_so_sources, + include_directories: [libpq_oauth_inc, postgres_inc], ++ c_args: libpq_so_c_args, + c_pch: pch_postgres_fe_h, + dependencies: [frontend_shlib_code, libpq, libpq_oauth_deps], + link_depends: export_file, @@ src/interfaces/libpq/fe-auth-oauth-curl.c => src/interfaces/libpq-oauth/oauth-cu -#include "libpq-int.h" #include "mb/pg_wchar.h" +#include "oauth-curl.h" ++#ifdef USE_DYNAMIC_OAUTH +#include "oauth-utils.h" ++#endif /* * It's generally prudent to set a maximum response size to buffer in memory, @@ src/interfaces/libpq-oauth/oauth-curl.c: prompt_user(struct async_ctx *actx, PGc if (!res) { +@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn) + { + fe_oauth_state *state = conn->sasl_state; + struct async_ctx *actx; ++ PQExpBuffer errbuf; + + if (!initialize_curl(conn)) + return PGRES_POLLING_FAILED; +@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn) + + error_return: + ++ /* ++ * For the dynamic module build, we can't safely rely on the offset of ++ * conn->errorMessage, since it depends on build options like USE_SSL et ++ * al. libpq gives us a translator function instead. ++ */ ++#ifdef USE_DYNAMIC_OAUTH ++ errbuf = conn_errorMessage(conn); ++#else ++ errbuf = &conn->errorMessage; ++#endif ++ + /* + * Assemble the three parts of our error: context, body, and detail. See + * also the documentation for struct async_ctx. + */ + if (actx->errctx) + { +- appendPQExpBufferStr(&conn->errorMessage, +- libpq_gettext(actx->errctx)); +- appendPQExpBufferStr(&conn->errorMessage, ": "); ++ appendPQExpBufferStr(errbuf, libpq_gettext(actx->errctx)); ++ appendPQExpBufferStr(errbuf, ": "); + } + + if (PQExpBufferDataBroken(actx->errbuf)) +- appendPQExpBufferStr(&conn->errorMessage, +- libpq_gettext("out of memory")); ++ appendPQExpBufferStr(errbuf, libpq_gettext("out of memory")); + else +- appendPQExpBufferStr(&conn->errorMessage, actx->errbuf.data); ++ appendPQExpBufferStr(errbuf, actx->errbuf.data); + + if (actx->curl_err[0]) + { + size_t len; + +- appendPQExpBuffer(&conn->errorMessage, +- " (libcurl: %s)", actx->curl_err); ++ appendPQExpBuffer(errbuf, " (libcurl: %s)", actx->curl_err); + + /* Sometimes libcurl adds a newline to the error buffer. :( */ +- len = conn->errorMessage.len; +- if (len >= 2 && conn->errorMessage.data[len - 2] == '\n') ++ len = errbuf->len; ++ if (len >= 2 && errbuf->data[len - 2] == '\n') + { +- conn->errorMessage.data[len - 2] = ')'; +- conn->errorMessage.data[len - 1] = '\0'; +- conn->errorMessage.len--; ++ errbuf->data[len - 2] = ')'; ++ errbuf->data[len - 1] = '\0'; ++ errbuf->len--; + } + } + +- appendPQExpBufferChar(&conn->errorMessage, '\n'); ++ appendPQExpBufferChar(errbuf, '\n'); + + return PGRES_POLLING_FAILED; + } ## src/interfaces/libpq-oauth/oauth-curl.h (new) ## @@ @@ src/interfaces/libpq-oauth/oauth-utils.c (new) +#include "libpq-int.h" +#include "oauth-utils.h" + ++#ifndef USE_DYNAMIC_OAUTH ++#error oauth-utils.c is not supported in static builds ++#endif ++ +static libpq_gettext_func libpq_gettext_impl; -+static conn_errorMessage_func conn_errorMessage; + +pgthreadlock_t pg_g_threadlock; ++conn_errorMessage_func conn_errorMessage; + +/*- + * Initializes libpq-oauth by setting necessary callbacks. @@ src/interfaces/libpq-oauth/oauth-utils.h (new) + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + ++/* Callback to safely obtain conn->errorMessage from a PGconn. */ ++extern conn_errorMessage_func conn_errorMessage; ++ +/* Duplicated APIs, copied from libpq. */ +extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3); +extern bool oauth_unsafe_debugging_enabled(void); @@ src/interfaces/libpq/Makefile: ifeq ($(with_ssl),openssl) +# libpq.so doesn't link against libcurl, but libpq.a needs libpq-oauth, and +# libpq-oauth needs libcurl. Put both into *.private. +PKG_CONFIG_REQUIRES_PRIVATE += libcurl -+%.pc: override SHLIB_LINK_INTERNAL += -lpq-oauth-$(MAJORVERSION) ++%.pc: override SHLIB_LINK_INTERNAL += -lpq-oauth +endif + all: all-lib libpq-refs-stamp @@ src/interfaces/libpq/fe-auth-oauth.c: cleanup_user_oauth_flow(PGconn *conn) + */ + const char *const module_name = +#if defined(__darwin__) -+ LIBDIR "/libpq-oauth-" PG_MAJORVERSION DLSUFFIX; ++ LIBDIR "/libpq-oauth-" PG_MAJORVERSION "-" PG_MINORVERSION DLSUFFIX; +#else -+ "libpq-oauth-" PG_MAJORVERSION DLSUFFIX; ++ "libpq-oauth-" PG_MAJORVERSION "-" PG_MINORVERSION DLSUFFIX; +#endif + + state->builtin_flow = dlopen(module_name, RTLD_NOW | RTLD_LOCAL); @@ src/interfaces/libpq/fe-auth-oauth.c: setup_token_request(PGconn *conn, fe_oauth - -#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 custom OAuth flows are available, and the builtin flow is not installed"); ++ libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)"); goto fail; - -#endif @@ src/interfaces/libpq/meson.build: libpq = declare_dependency( + # libpq-oauth needs libcurl. Put both into *.private. + private_deps += [ + libpq_oauth_deps, -+ '-lpq-oauth-@0@'.format(pg_version_major), ++ '-lpq-oauth', + ] +endif + @@ src/test/modules/oauth_validator/t/002_client.pl: if ($ENV{with_libcurl} ne 'yes flags => ["--no-hook"], expected_stderr => - qr/no custom OAuth flows are available, and libpq was not built with libcurl support/ -+ qr/no custom OAuth flows are available, and the builtin flow is not installed/ ++ qr/no OAuth flows are available \(try installing the libpq-oauth package\)/ ); } Attachments: [text/plain] since-v7.diff.txt (13.0K, ../../CAOYmi+k9uN2OQ_sXjztZYPSpPbfGmkkv4yhtPhyjNQ6vy7SSfA@mail.gmail.com/2-since-v7.diff.txt) download | inline: -: ----------- > 1: 5f87f11b18e Add minor-version counterpart to (PG_)MAJORVERSION 1: 942ad5391e2 ! 2: 4c9cc7f69af oauth: Move the builtin flow into a separate module @@ Commit message the search path came from a different build of Postgres. 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 major 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.) + symlinks, and it's named libpq-oauth-<major>-<minor>.so to avoid mixing + and matching across Postgres versions, in case internal struct order + needs to change. (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 @@ src/interfaces/libpq-oauth/Makefile (new) + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. -+NAME = pq-oauth-$(MAJORVERSION) ++NAME = pq-oauth-$(MAJORVERSION)-$(MINORVERSION) + -+# Force the name "libpq-oauth" for both the static and shared libraries. ++# Force the name "libpq-oauth" for both the static and shared libraries. The ++# staticlib doesn't need version information in its name. +override shlib := lib$(NAME)$(DLSUFFIX) -+override stlib := lib$(NAME).a ++override stlib := libpq-oauth.a + +override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(LIBCURL_CPPFLAGS) $(CPPFLAGS) + +OBJS = \ -+ $(WIN32RES) \ -+ oauth-curl.o ++ $(WIN32RES) ++ ++OBJS_STATIC = oauth-curl.o + +# The shared library needs additional glue symbols. -+$(shlib): OBJS += oauth-utils.o -+$(shlib): oauth-utils.o ++OBJS_SHLIB = \ ++ oauth-curl_shlib.o \ ++ oauth-utils.o \ ++ ++oauth-utils.o: override CPPFLAGS += -DUSE_DYNAMIC_OAUTH ++oauth-curl_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH ++ ++# Add shlib-/stlib-specific objects. ++$(shlib): override OBJS += $(OBJS_SHLIB) ++$(shlib): $(OBJS_SHLIB) ++ ++$(stlib): override OBJS += $(OBJS_STATIC) ++$(stlib): $(OBJS_STATIC) + +SHLIB_LINK_INTERNAL = $(libpq_pgport_shlib) +SHLIB_LINK = $(LIBCURL_LDFLAGS) $(LIBCURL_LDLIBS) @@ src/interfaces/libpq-oauth/Makefile (new) +# Shared library stuff +include $(top_srcdir)/src/Makefile.shlib + ++# Use src/common/Makefile's trick for tracking dependencies of shlib-specific ++# objects. ++%_shlib.o: %.c %.o ++ $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@ ++ +# Ignore the standard rules for SONAME-less installation; we want both the +# static and shared libraries to go into libdir. +install: all installdirs $(stlib) $(shlib) @@ src/interfaces/libpq-oauth/Makefile (new) + rm -f '$(DESTDIR)$(libdir)/$(shlib)' + +clean distclean: clean-lib -+ rm -f $(OBJS) oauth-utils.o ++ rm -f $(OBJS) $(OBJS_STATIC) $(OBJS_SHLIB) ## src/interfaces/libpq-oauth/README (new) ## @@ @@ src/interfaces/libpq-oauth/README (new) += Load-Time ABI = + +This module ABI is an internal implementation detail, so it's subject to change -+across major releases; the name of the module (libpq-oauth-MAJOR) reflects this. ++across releases; the name of the module (libpq-oauth-MAJOR-MINOR) reflects this. +The module exports the following symbols: + +- PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); @@ src/interfaces/libpq-oauth/README (new) + +At the moment, pg_fe_run_oauth_flow() relies on libpq's pg_g_threadlock and +libpq_gettext(), which must be injected by libpq using this initialization -+function before the flow is run. It also relies on libpq to expose -+conn->errorMessage, via the errmsg_impl. ++function before the flow is run. + -+This dependency injection is done to ensure that the module ABI is decoupled -+from the internals of `struct pg_conn`. This way we can safely search the -+standard dlopen() paths (e.g. RPATH, LD_LIBRARY_PATH, the SO cache) for an -+implementation module to use, even if that module wasn't compiled at the same -+time as libpq. ++It also relies on libpq to expose conn->errorMessage, via the errmsg_impl. This ++is done to decouple the module ABI from the offset of errorMessage, which can ++change positions depending on configure-time options. This way we can safely ++search the standard dlopen() paths (e.g. RPATH, LD_LIBRARY_PATH, the SO cache) ++for an implementation module to use, even if that module wasn't compiled at the ++same time as libpq. + += Static Build = + +The static library libpq.a does not perform any dynamic loading. If the builtin -+flow is enabled, the application is expected to link against libpq-oauth-*.a ++flow is enabled, the application is expected to link against libpq-oauth.a +directly to provide the necessary symbols. ## src/interfaces/libpq-oauth/exports.txt (new) ## @@ src/interfaces/libpq-oauth/meson.build (new) +libpq_oauth_so_sources = files( + 'oauth-utils.c', +) ++libpq_oauth_so_c_args = ['-DUSE_DYNAMIC_OAUTH'] + +export_file = custom_target('libpq-oauth.exports', + kwargs: gen_export_kwargs, @@ src/interfaces/libpq-oauth/meson.build (new) +# port needs to be in include path due to pthread-win32.h +libpq_oauth_inc = include_directories('.', '../libpq', '../../port') + -+# This is an internal module; we don't want an SONAME and therefore do not set -+# SO_MAJOR_VERSION. -+libpq_oauth_name = 'libpq-oauth-@0@'.format(pg_version_major) -+ -+libpq_oauth_st = static_library(libpq_oauth_name, ++libpq_oauth_st = static_library('libpq-oauth', + libpq_oauth_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_pch: pch_postgres_fe_h, @@ src/interfaces/libpq-oauth/meson.build (new) + kwargs: default_lib_args, +) + ++# This is an internal module; we don't want an SONAME and therefore do not set ++# SO_MAJOR_VERSION. ++libpq_oauth_name = 'libpq-oauth-@0@-@1@'.format(pg_version_major, pg_version_minor) ++ +libpq_oauth_so = shared_module(libpq_oauth_name, + libpq_oauth_sources + libpq_oauth_so_sources, + include_directories: [libpq_oauth_inc, postgres_inc], ++ c_args: libpq_so_c_args, + c_pch: pch_postgres_fe_h, + dependencies: [frontend_shlib_code, libpq, libpq_oauth_deps], + link_depends: export_file, @@ src/interfaces/libpq/fe-auth-oauth-curl.c => src/interfaces/libpq-oauth/oauth-cu -#include "libpq-int.h" #include "mb/pg_wchar.h" +#include "oauth-curl.h" ++#ifdef USE_DYNAMIC_OAUTH +#include "oauth-utils.h" ++#endif /* * It's generally prudent to set a maximum response size to buffer in memory, @@ src/interfaces/libpq-oauth/oauth-curl.c: prompt_user(struct async_ctx *actx, PGc if (!res) { +@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn) + { + fe_oauth_state *state = conn->sasl_state; + struct async_ctx *actx; ++ PQExpBuffer errbuf; + + if (!initialize_curl(conn)) + return PGRES_POLLING_FAILED; +@@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn) + + error_return: + ++ /* ++ * For the dynamic module build, we can't safely rely on the offset of ++ * conn->errorMessage, since it depends on build options like USE_SSL et ++ * al. libpq gives us a translator function instead. ++ */ ++#ifdef USE_DYNAMIC_OAUTH ++ errbuf = conn_errorMessage(conn); ++#else ++ errbuf = &conn->errorMessage; ++#endif ++ + /* + * Assemble the three parts of our error: context, body, and detail. See + * also the documentation for struct async_ctx. + */ + if (actx->errctx) + { +- appendPQExpBufferStr(&conn->errorMessage, +- libpq_gettext(actx->errctx)); +- appendPQExpBufferStr(&conn->errorMessage, ": "); ++ appendPQExpBufferStr(errbuf, libpq_gettext(actx->errctx)); ++ appendPQExpBufferStr(errbuf, ": "); + } + + if (PQExpBufferDataBroken(actx->errbuf)) +- appendPQExpBufferStr(&conn->errorMessage, +- libpq_gettext("out of memory")); ++ appendPQExpBufferStr(errbuf, libpq_gettext("out of memory")); + else +- appendPQExpBufferStr(&conn->errorMessage, actx->errbuf.data); ++ appendPQExpBufferStr(errbuf, actx->errbuf.data); + + if (actx->curl_err[0]) + { + size_t len; + +- appendPQExpBuffer(&conn->errorMessage, +- " (libcurl: %s)", actx->curl_err); ++ appendPQExpBuffer(errbuf, " (libcurl: %s)", actx->curl_err); + + /* Sometimes libcurl adds a newline to the error buffer. :( */ +- len = conn->errorMessage.len; +- if (len >= 2 && conn->errorMessage.data[len - 2] == '\n') ++ len = errbuf->len; ++ if (len >= 2 && errbuf->data[len - 2] == '\n') + { +- conn->errorMessage.data[len - 2] = ')'; +- conn->errorMessage.data[len - 1] = '\0'; +- conn->errorMessage.len--; ++ errbuf->data[len - 2] = ')'; ++ errbuf->data[len - 1] = '\0'; ++ errbuf->len--; + } + } + +- appendPQExpBufferChar(&conn->errorMessage, '\n'); ++ appendPQExpBufferChar(errbuf, '\n'); + + return PGRES_POLLING_FAILED; + } ## src/interfaces/libpq-oauth/oauth-curl.h (new) ## @@ @@ src/interfaces/libpq-oauth/oauth-utils.c (new) +#include "libpq-int.h" +#include "oauth-utils.h" + ++#ifndef USE_DYNAMIC_OAUTH ++#error oauth-utils.c is not supported in static builds ++#endif ++ +static libpq_gettext_func libpq_gettext_impl; -+static conn_errorMessage_func conn_errorMessage; + +pgthreadlock_t pg_g_threadlock; ++conn_errorMessage_func conn_errorMessage; + +/*- + * Initializes libpq-oauth by setting necessary callbacks. @@ src/interfaces/libpq-oauth/oauth-utils.h (new) + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + ++/* Callback to safely obtain conn->errorMessage from a PGconn. */ ++extern conn_errorMessage_func conn_errorMessage; ++ +/* Duplicated APIs, copied from libpq. */ +extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3); +extern bool oauth_unsafe_debugging_enabled(void); @@ src/interfaces/libpq/Makefile: ifeq ($(with_ssl),openssl) +# libpq.so doesn't link against libcurl, but libpq.a needs libpq-oauth, and +# libpq-oauth needs libcurl. Put both into *.private. +PKG_CONFIG_REQUIRES_PRIVATE += libcurl -+%.pc: override SHLIB_LINK_INTERNAL += -lpq-oauth-$(MAJORVERSION) ++%.pc: override SHLIB_LINK_INTERNAL += -lpq-oauth +endif + all: all-lib libpq-refs-stamp @@ src/interfaces/libpq/fe-auth-oauth.c: cleanup_user_oauth_flow(PGconn *conn) + */ + const char *const module_name = +#if defined(__darwin__) -+ LIBDIR "/libpq-oauth-" PG_MAJORVERSION DLSUFFIX; ++ LIBDIR "/libpq-oauth-" PG_MAJORVERSION "-" PG_MINORVERSION DLSUFFIX; +#else -+ "libpq-oauth-" PG_MAJORVERSION DLSUFFIX; ++ "libpq-oauth-" PG_MAJORVERSION "-" PG_MINORVERSION DLSUFFIX; +#endif + + state->builtin_flow = dlopen(module_name, RTLD_NOW | RTLD_LOCAL); @@ src/interfaces/libpq/fe-auth-oauth.c: setup_token_request(PGconn *conn, fe_oauth - -#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 custom OAuth flows are available, and the builtin flow is not installed"); ++ libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)"); goto fail; - -#endif @@ src/interfaces/libpq/meson.build: libpq = declare_dependency( + # libpq-oauth needs libcurl. Put both into *.private. + private_deps += [ + libpq_oauth_deps, -+ '-lpq-oauth-@0@'.format(pg_version_major), ++ '-lpq-oauth', + ] +endif + @@ src/test/modules/oauth_validator/t/002_client.pl: if ($ENV{with_libcurl} ne 'yes flags => ["--no-hook"], expected_stderr => - qr/no custom OAuth flows are available, and libpq was not built with libcurl support/ -+ qr/no custom OAuth flows are available, and the builtin flow is not installed/ ++ qr/no OAuth flows are available \(try installing the libpq-oauth package\)/ ); } [application/x-patch] v8-0001-Add-minor-version-counterpart-to-PG_-MAJORVERSION.patch (3.5K, ../../CAOYmi+k9uN2OQ_sXjztZYPSpPbfGmkkv4yhtPhyjNQ6vy7SSfA@mail.gmail.com/3-v8-0001-Add-minor-version-counterpart-to-PG_-MAJORVERSION.patch) download | inline diff: From 5f87f11b18ea83615c342c832caace49bf7e3897 Mon Sep 17 00:00:00 2001 From: Jacob Champion <[email protected]> Date: Mon, 21 Apr 2025 13:43:08 -0700 Subject: [PATCH v8 1/2] Add minor-version counterpart to (PG_)MAJORVERSION An upcoming commit will name a library, libpq-oauth, using the major and minor versions. Make the minor version accessible from the Makefiles and as a string constant in the code. --- configure | 7 +++++++ configure.ac | 2 ++ meson.build | 1 + src/Makefile.global.in | 1 + src/include/pg_config.h.in | 3 +++ src/makefiles/meson.build | 1 + 6 files changed, 15 insertions(+) diff --git a/configure b/configure index 0936010718d..3d783793dfa 100755 --- a/configure +++ b/configure @@ -792,6 +792,7 @@ build_os build_vendor build_cpu build +PG_MINORVERSION PG_MAJORVERSION target_alias host_alias @@ -2877,6 +2878,12 @@ cat >>confdefs.h <<_ACEOF _ACEOF + +cat >>confdefs.h <<_ACEOF +#define PG_MINORVERSION "$PG_MINORVERSION" +_ACEOF + + cat >>confdefs.h <<_ACEOF #define PG_MINORVERSION_NUM $PG_MINORVERSION _ACEOF diff --git a/configure.ac b/configure.ac index 2a78cddd825..1cb3a0ff042 100644 --- a/configure.ac +++ b/configure.ac @@ -35,6 +35,8 @@ test -n "$PG_MINORVERSION" || PG_MINORVERSION=0 AC_SUBST(PG_MAJORVERSION) AC_DEFINE_UNQUOTED(PG_MAJORVERSION, "$PG_MAJORVERSION", [PostgreSQL major version as a string]) AC_DEFINE_UNQUOTED(PG_MAJORVERSION_NUM, $PG_MAJORVERSION, [PostgreSQL major version number]) +AC_SUBST(PG_MINORVERSION) +AC_DEFINE_UNQUOTED(PG_MINORVERSION, "$PG_MINORVERSION", [PostgreSQL minor version as a string]) AC_DEFINE_UNQUOTED(PG_MINORVERSION_NUM, $PG_MINORVERSION, [PostgreSQL minor version number]) PGAC_ARG_REQ(with, extra-version, [STRING], [append STRING to version], diff --git a/meson.build b/meson.build index a1516e54529..18423a7c13e 100644 --- a/meson.build +++ b/meson.build @@ -148,6 +148,7 @@ pg_version += get_option('extra_version') cdata.set_quoted('PG_VERSION', pg_version) cdata.set_quoted('PG_MAJORVERSION', pg_version_major.to_string()) cdata.set('PG_MAJORVERSION_NUM', pg_version_major) +cdata.set_quoted('PG_MINORVERSION', pg_version_minor.to_string()) cdata.set('PG_MINORVERSION_NUM', pg_version_minor) cdata.set('PG_VERSION_NUM', pg_version_num) # PG_VERSION_STR is built later, it depends on compiler test results diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 6722fbdf365..54b4a07712e 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -40,6 +40,7 @@ maintainer-clean: distclean # PostgreSQL version number VERSION = @PACKAGE_VERSION@ MAJORVERSION = @PG_MAJORVERSION@ +MINORVERSION = @PG_MINORVERSION@ VERSION_NUM = @PG_VERSION_NUM@ PACKAGE_URL = @PACKAGE_URL@ diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in index c3cc9fa856d..4fe37d228c5 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -602,6 +602,9 @@ /* PostgreSQL major version number */ #undef PG_MAJORVERSION_NUM +/* PostgreSQL minor version as a string */ +#undef PG_MINORVERSION + /* PostgreSQL minor version number */ #undef PG_MINORVERSION_NUM diff --git a/src/makefiles/meson.build b/src/makefiles/meson.build index 55da678ec27..e3adb5d8dc4 100644 --- a/src/makefiles/meson.build +++ b/src/makefiles/meson.build @@ -36,6 +36,7 @@ pgxs_kv = { 'PACKAGE_URL': pg_url, 'PACKAGE_VERSION': pg_version, 'PG_MAJORVERSION': pg_version_major, + 'PG_MINORVERSION': pg_version_minor, 'PG_VERSION_NUM': pg_version_num, 'configure_input': 'meson', -- 2.34.1 [application/x-patch] v8-0002-oauth-Move-the-builtin-flow-into-a-separate-modul.patch (56.7K, ../../CAOYmi+k9uN2OQ_sXjztZYPSpPbfGmkkv4yhtPhyjNQ6vy7SSfA@mail.gmail.com/4-v8-0002-oauth-Move-the-builtin-flow-into-a-separate-modul.patch) download | inline diff: From 4c9cc7f69afd8f59f98b38900e53fc199d4b4009 Mon Sep 17 00:00:00 2001 From: Jacob Champion <[email protected]> Date: Wed, 26 Mar 2025 10:55:28 -0700 Subject: [PATCH v8 2/2] 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 offset of conn->errorMessage to libpq-oauth, so that we can function without crashing if the module on the search path came from a different build of Postgres. This ABI is considered "private". The module has no SONAME or version symlinks, and it's named libpq-oauth-<major>-<minor>.so to avoid mixing and matching across Postgres versions, in case internal struct order needs to change. (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 many people reviewed the design and implementation; thank you! Co-authored-by: Daniel Gustafsson <[email protected]> Reviewed-by: Andres Freund <[email protected]> Reviewed-by: Christoph Berg <[email protected]> Reviewed-by: Jelte Fennema-Nio <[email protected]> Reviewed-by: Peter Eisentraut <[email protected]> Reviewed-by: Wolfgang Walther <[email protected]> Discussion: https://postgr.es/m/641687.1742360249%40sss.pgh.pa.us --- config/programs.m4 | 17 +- configure | 50 ++++- configure.ac | 26 ++- doc/src/sgml/installation.sgml | 8 + doc/src/sgml/libpq.sgml | 30 ++- meson.build | 32 ++- src/Makefile.global.in | 3 + src/interfaces/Makefile | 12 ++ src/interfaces/libpq-oauth/Makefile | 83 +++++++ src/interfaces/libpq-oauth/README | 43 ++++ src/interfaces/libpq-oauth/exports.txt | 4 + src/interfaces/libpq-oauth/meson.build | 45 ++++ .../oauth-curl.c} | 99 +++++---- src/interfaces/libpq-oauth/oauth-curl.h | 24 +++ src/interfaces/libpq-oauth/oauth-utils.c | 202 ++++++++++++++++++ src/interfaces/libpq-oauth/oauth-utils.h | 38 ++++ src/interfaces/libpq/Makefile | 36 +++- src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-auth-oauth.c | 197 ++++++++++++++++- src/interfaces/libpq/fe-auth-oauth.h | 5 +- src/interfaces/libpq/meson.build | 25 ++- src/interfaces/libpq/nls.mk | 12 +- src/makefiles/meson.build | 2 + src/test/modules/oauth_validator/meson.build | 2 +- .../modules/oauth_validator/t/002_client.pl | 2 +- 25 files changed, 886 insertions(+), 112 deletions(-) create mode 100644 src/interfaces/libpq-oauth/Makefile create mode 100644 src/interfaces/libpq-oauth/README create mode 100644 src/interfaces/libpq-oauth/exports.txt create mode 100644 src/interfaces/libpq-oauth/meson.build rename src/interfaces/{libpq/fe-auth-oauth-curl.c => libpq-oauth/oauth-curl.c} (97%) create mode 100644 src/interfaces/libpq-oauth/oauth-curl.h create mode 100644 src/interfaces/libpq-oauth/oauth-utils.c create mode 100644 src/interfaces/libpq-oauth/oauth-utils.h diff --git a/config/programs.m4 b/config/programs.m4 index 0a07feb37cc..0ad1e58b48d 100644 --- a/config/programs.m4 +++ b/config/programs.m4 @@ -286,9 +286,20 @@ AC_DEFUN([PGAC_CHECK_LIBCURL], [ AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file <curl/curl.h> is required for --with-libcurl])]) - AC_CHECK_LIB(curl, curl_multi_init, [], + AC_CHECK_LIB(curl, curl_multi_init, [ + AC_DEFINE([HAVE_LIBCURL], [1], [Define to 1 if you have the `curl' library (-lcurl).]) + AC_SUBST(LIBCURL_LDLIBS, -lcurl) + ], [AC_MSG_ERROR([library 'curl' does not provide curl_multi_init])]) + pgac_save_CPPFLAGS=$CPPFLAGS + pgac_save_LDFLAGS=$LDFLAGS + pgac_save_LIBS=$LIBS + + CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" + LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS" + LIBS="$LIBCURL_LDLIBS $LIBS" + # Check to see whether the current platform supports threadsafe Curl # initialization. AC_CACHE_CHECK([for curl_global_init thread safety], [pgac_cv__libcurl_threadsafe_init], @@ -338,4 +349,8 @@ AC_DEFUN([PGAC_CHECK_LIBCURL], *** lookups. Rebuild libcurl with the AsynchDNS feature enabled in order *** to use it with libpq.]) fi + + CPPFLAGS=$pgac_save_CPPFLAGS + LDFLAGS=$pgac_save_LDFLAGS + LIBS=$pgac_save_LIBS ])# PGAC_CHECK_LIBCURL diff --git a/configure b/configure index 3d783793dfa..eedd18e6d9a 100755 --- a/configure +++ b/configure @@ -655,6 +655,7 @@ UUID_LIBS LDAP_LIBS_BE LDAP_LIBS_FE with_ssl +LIBCURL_LDLIBS PTHREAD_CFLAGS PTHREAD_LIBS PTHREAD_CC @@ -711,6 +712,8 @@ with_libxml LIBNUMA_LIBS LIBNUMA_CFLAGS with_libnuma +LIBCURL_LDFLAGS +LIBCURL_CPPFLAGS LIBCURL_LIBS LIBCURL_CFLAGS with_libcurl @@ -9060,19 +9063,27 @@ $as_echo "yes" >&6; } fi - # We only care about -I, -D, and -L switches; - # note that -lcurl will be added by PGAC_CHECK_LIBCURL below. + # Curl's flags are kept separate from the standard CPPFLAGS/LDFLAGS. We use + # them only for libpq-oauth. + LIBCURL_CPPFLAGS= + LIBCURL_LDFLAGS= + + # We only care about -I, -D, and -L switches. Note that -lcurl will be added + # to LIBCURL_LDLIBS by PGAC_CHECK_LIBCURL, below. for pgac_option in $LIBCURL_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) LIBCURL_CPPFLAGS="$LIBCURL_CPPFLAGS $pgac_option";; esac done for pgac_option in $LIBCURL_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBCURL_LDFLAGS="$LIBCURL_LDFLAGS $pgac_option";; esac done + + + # OAuth requires python for testing if test "$with_python" != yes; then { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: *** OAuth support tests require --with-python to run" >&5 @@ -12711,9 +12722,6 @@ fi fi -# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults -# during gss_acquire_cred(). This is possibly related to Curl's Heimdal -# dependency on that platform? if test "$with_libcurl" = yes ; then ac_fn_c_check_header_mongrel "$LINENO" "curl/curl.h" "ac_cv_header_curl_curl_h" "$ac_includes_default" @@ -12761,17 +12769,26 @@ fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curl_curl_multi_init" >&5 $as_echo "$ac_cv_lib_curl_curl_multi_init" >&6; } if test "x$ac_cv_lib_curl_curl_multi_init" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LIBCURL 1 -_ACEOF - LIBS="-lcurl $LIBS" + +$as_echo "#define HAVE_LIBCURL 1" >>confdefs.h + + LIBCURL_LDLIBS=-lcurl + else as_fn_error $? "library 'curl' does not provide curl_multi_init" "$LINENO" 5 fi + pgac_save_CPPFLAGS=$CPPFLAGS + pgac_save_LDFLAGS=$LDFLAGS + pgac_save_LIBS=$LIBS + + CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" + LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS" + LIBS="$LIBCURL_LDLIBS $LIBS" + # Check to see whether the current platform supports threadsafe Curl # initialization. { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_global_init thread safety" >&5 @@ -12875,6 +12892,10 @@ $as_echo "$pgac_cv__libcurl_async_dns" >&6; } *** to use it with libpq." "$LINENO" 5 fi + CPPFLAGS=$pgac_save_CPPFLAGS + LDFLAGS=$pgac_save_LDFLAGS + LIBS=$pgac_save_LIBS + fi if test "$with_gssapi" = yes ; then @@ -14523,6 +14544,13 @@ done fi +if test "$with_libcurl" = yes ; then + # Error out early if this platform can't support libpq-oauth. + if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then + as_fn_error $? "client OAuth is not supported on this platform" "$LINENO" 5 + fi +fi + ## ## Types, structures, compiler characteristics ## diff --git a/configure.ac b/configure.ac index 1cb3a0ff042..7329b23d309 100644 --- a/configure.ac +++ b/configure.ac @@ -1035,19 +1035,27 @@ if test "$with_libcurl" = yes ; then # to explicitly set TLS 1.3 ciphersuites). PKG_CHECK_MODULES(LIBCURL, [libcurl >= 7.61.0]) - # We only care about -I, -D, and -L switches; - # note that -lcurl will be added by PGAC_CHECK_LIBCURL below. + # Curl's flags are kept separate from the standard CPPFLAGS/LDFLAGS. We use + # them only for libpq-oauth. + LIBCURL_CPPFLAGS= + LIBCURL_LDFLAGS= + + # We only care about -I, -D, and -L switches. Note that -lcurl will be added + # to LIBCURL_LDLIBS by PGAC_CHECK_LIBCURL, below. for pgac_option in $LIBCURL_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) LIBCURL_CPPFLAGS="$LIBCURL_CPPFLAGS $pgac_option";; esac done for pgac_option in $LIBCURL_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBCURL_LDFLAGS="$LIBCURL_LDFLAGS $pgac_option";; esac done + AC_SUBST(LIBCURL_CPPFLAGS) + AC_SUBST(LIBCURL_LDFLAGS) + # OAuth requires python for testing if test "$with_python" != yes; then AC_MSG_WARN([*** OAuth support tests require --with-python to run]) @@ -1356,9 +1364,6 @@ failure. It is possible the compiler isn't looking in the proper directory. Use --without-zlib to disable zlib support.])]) fi -# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults -# during gss_acquire_cred(). This is possibly related to Curl's Heimdal -# dependency on that platform? if test "$with_libcurl" = yes ; then PGAC_CHECK_LIBCURL fi @@ -1656,6 +1661,13 @@ if test "$PORTNAME" = "win32" ; then AC_CHECK_HEADERS(crtdefs.h) fi +if test "$with_libcurl" = yes ; then + # Error out early if this platform can't support libpq-oauth. + if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then + AC_MSG_ERROR([client OAuth is not supported on this platform]) + fi +fi + ## ## Types, structures, compiler characteristics ## diff --git a/doc/src/sgml/installation.sgml b/doc/src/sgml/installation.sgml index 077bcc20759..d928b103d22 100644 --- a/doc/src/sgml/installation.sgml +++ b/doc/src/sgml/installation.sgml @@ -313,6 +313,14 @@ </para> </listitem> + <listitem> + <para> + You need <productname>Curl</productname> to build an optional module + which implements the <link linkend="libpq-oauth">OAuth Device + Authorization flow</link> for client applications. + </para> + </listitem> + <listitem> <para> You need <productname>LZ4</productname>, if you want to support diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 3be66789ba7..cd748902f4d 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -10226,15 +10226,20 @@ void PQinitSSL(int do_ssl); <title>OAuth Support</title> <para> - libpq implements support for the OAuth v2 Device Authorization client flow, + <application>libpq</application> implements support for the OAuth v2 Device Authorization client flow, documented in <ulink url="https://datatracker.ietf.org/doc/html/rfc8628">RFC 8628</ulink>, - which it will attempt to use by default if the server + as an optional module. See the <link linkend="configure-option-with-libcurl"> + installation documentation</link> for information on how to enable support + for Device Authorization as a builtin flow. + </para> + <para> + When support is enabled and the optional module installed, <application>libpq</application> + will use the builtin flow by default if the server <link linkend="auth-oauth">requests a bearer token</link> during authentication. This flow can be utilized even if the system running the client application does not have a usable web browser, for example when - running a client via <application>SSH</application>. Client applications may implement their own flows - instead; see <xref linkend="libpq-oauth-authdata-hooks"/>. + running a client via <acronym>SSH</acronym>. </para> <para> The builtin flow will, by default, print a URL to visit and a user code to @@ -10251,6 +10256,11 @@ Visit https://example.com/device and enter the code: ABCD-EFGH they match expectations, before continuing. Permissions should not be given to untrusted third parties. </para> + <para> + Client applications may implement their own flows to customize interaction + and integration with applications. See <xref linkend="libpq-oauth-authdata-hooks"/> + for more information on how add a custom flow to <application>libpq</application>. + </para> <para> For an OAuth client flow to be usable, the connection string must at minimum contain <xref linkend="libpq-connect-oauth-issuer"/> and @@ -10366,7 +10376,9 @@ typedef struct _PGpromptOAuthDevice </synopsis> </para> <para> - The OAuth Device Authorization flow included in <application>libpq</application> + The OAuth Device Authorization flow which + <link linkend="configure-option-with-libcurl">can be included</link> + in <application>libpq</application> requires the end user to visit a URL with a browser, then enter a code which permits <application>libpq</application> to connect to the server on their behalf. The default prompt simply prints the @@ -10378,7 +10390,8 @@ typedef struct _PGpromptOAuthDevice This callback is only invoked during the builtin device authorization flow. If the application installs a <link linkend="libpq-oauth-authdata-oauth-bearer-token">custom OAuth - flow</link>, this authdata type will not be used. + flow</link>, or <application>libpq</application> was not built with + support for the builtin flow, this authdata type will not be used. </para> <para> If a non-NULL <structfield>verification_uri_complete</structfield> is @@ -10400,8 +10413,9 @@ typedef struct _PGpromptOAuthDevice </term> <listitem> <para> - Replaces the entire OAuth flow with a custom implementation. The hook - should either directly return a Bearer token for the current + Adds a custom implementation of a flow, replacing the builtin flow if + it is <link linkend="configure-option-with-libcurl">installed</link>. + The hook should either directly return a Bearer token for the current user/issuer/scope combination, if one is available without blocking, or else set up an asynchronous callback to retrieve one. </para> diff --git a/meson.build b/meson.build index 18423a7c13e..6787683ca27 100644 --- a/meson.build +++ b/meson.build @@ -107,6 +107,7 @@ os_deps = [] backend_both_deps = [] backend_deps = [] libpq_deps = [] +libpq_oauth_deps = [] pg_sysroot = '' @@ -861,13 +862,13 @@ endif ############################################################### libcurlopt = get_option('libcurl') +oauth_flow_supported = false + if not libcurlopt.disabled() # Check for libcurl 7.61.0 or higher (corresponding to RHEL8 and the ability # to explicitly set TLS 1.3 ciphersuites). libcurl = dependency('libcurl', version: '>= 7.61.0', required: libcurlopt) if libcurl.found() - cdata.set('USE_LIBCURL', 1) - # Check to see whether the current platform supports thread-safe Curl # initialization. libcurl_threadsafe_init = false @@ -939,6 +940,22 @@ if not libcurlopt.disabled() endif endif + # Check that the current platform supports our builtin flow. This requires + # libcurl and one of either epoll or kqueue. + oauth_flow_supported = ( + libcurl.found() + and (cc.check_header('sys/event.h', required: false, + args: test_c_args, include_directories: postgres_inc) + or cc.check_header('sys/epoll.h', required: false, + args: test_c_args, include_directories: postgres_inc)) + ) + + if oauth_flow_supported + cdata.set('USE_LIBCURL', 1) + elif libcurlopt.enabled() + error('client OAuth is not supported on this platform') + endif + else libcurl = not_found_dep endif @@ -3273,17 +3290,18 @@ libpq_deps += [ gssapi, ldap_r, - # XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults - # during gss_acquire_cred(). This is possibly related to Curl's Heimdal - # dependency on that platform? - libcurl, libintl, ssl, ] +libpq_oauth_deps += [ + libcurl, +] + subdir('src/interfaces/libpq') -# fe_utils depends on libpq +# fe_utils and libpq-oauth depends on libpq subdir('src/fe_utils') +subdir('src/interfaces/libpq-oauth') # for frontend binaries frontend_code = declare_dependency( diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 54b4a07712e..f4caece04df 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -348,6 +348,9 @@ perl_embed_ldflags = @perl_embed_ldflags@ AWK = @AWK@ LN_S = @LN_S@ +LIBCURL_CPPFLAGS = @LIBCURL_CPPFLAGS@ +LIBCURL_LDFLAGS = @LIBCURL_LDFLAGS@ +LIBCURL_LDLIBS = @LIBCURL_LDLIBS@ MSGFMT = @MSGFMT@ MSGFMT_FLAGS = @MSGFMT_FLAGS@ MSGMERGE = @MSGMERGE@ diff --git a/src/interfaces/Makefile b/src/interfaces/Makefile index 7d56b29d28f..e6822caa206 100644 --- a/src/interfaces/Makefile +++ b/src/interfaces/Makefile @@ -14,7 +14,19 @@ include $(top_builddir)/src/Makefile.global SUBDIRS = libpq ecpg +ifeq ($(with_libcurl), yes) +SUBDIRS += libpq-oauth +else +ALWAYS_SUBDIRS += libpq-oauth +endif + $(recurse) +$(recurse_always) all-ecpg-recurse: all-libpq-recurse install-ecpg-recurse: install-libpq-recurse + +ifeq ($(with_libcurl), yes) +all-libpq-oauth-recurse: all-libpq-recurse +install-libpq-oauth-recurse: install-libpq-recurse +endif diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile new file mode 100644 index 00000000000..98acaff1a3b --- /dev/null +++ b/src/interfaces/libpq-oauth/Makefile @@ -0,0 +1,83 @@ +#------------------------------------------------------------------------- +# +# Makefile for libpq-oauth +# +# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1994, Regents of the University of California +# +# src/interfaces/libpq-oauth/Makefile +# +#------------------------------------------------------------------------- + +subdir = src/interfaces/libpq-oauth +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +PGFILEDESC = "libpq-oauth - device authorization OAuth support" + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. +NAME = pq-oauth-$(MAJORVERSION)-$(MINORVERSION) + +# Force the name "libpq-oauth" for both the static and shared libraries. The +# staticlib doesn't need version information in its name. +override shlib := lib$(NAME)$(DLSUFFIX) +override stlib := libpq-oauth.a + +override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(LIBCURL_CPPFLAGS) $(CPPFLAGS) + +OBJS = \ + $(WIN32RES) + +OBJS_STATIC = oauth-curl.o + +# The shared library needs additional glue symbols. +OBJS_SHLIB = \ + oauth-curl_shlib.o \ + oauth-utils.o \ + +oauth-utils.o: override CPPFLAGS += -DUSE_DYNAMIC_OAUTH +oauth-curl_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH + +# Add shlib-/stlib-specific objects. +$(shlib): override OBJS += $(OBJS_SHLIB) +$(shlib): $(OBJS_SHLIB) + +$(stlib): override OBJS += $(OBJS_STATIC) +$(stlib): $(OBJS_STATIC) + +SHLIB_LINK_INTERNAL = $(libpq_pgport_shlib) +SHLIB_LINK = $(LIBCURL_LDFLAGS) $(LIBCURL_LDLIBS) +SHLIB_PREREQS = submake-libpq +SHLIB_EXPORTS = exports.txt + +# Disable -bundle_loader on macOS. +BE_DLLLIBS = + +# By default, a library without an SONAME doesn't get a static library, so we +# add it to the build explicitly. +all: all-lib all-static-lib + +# Shared library stuff +include $(top_srcdir)/src/Makefile.shlib + +# Use src/common/Makefile's trick for tracking dependencies of shlib-specific +# objects. +%_shlib.o: %.c %.o + $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@ + +# Ignore the standard rules for SONAME-less installation; we want both the +# static and shared libraries to go into libdir. +install: all installdirs $(stlib) $(shlib) + $(INSTALL_SHLIB) $(shlib) '$(DESTDIR)$(libdir)/$(shlib)' + $(INSTALL_STLIB) $(stlib) '$(DESTDIR)$(libdir)/$(stlib)' + +installdirs: + $(MKDIR_P) '$(DESTDIR)$(libdir)' + +uninstall: + rm -f '$(DESTDIR)$(libdir)/$(stlib)' + rm -f '$(DESTDIR)$(libdir)/$(shlib)' + +clean distclean: clean-lib + rm -f $(OBJS) $(OBJS_STATIC) $(OBJS_SHLIB) diff --git a/src/interfaces/libpq-oauth/README b/src/interfaces/libpq-oauth/README new file mode 100644 index 00000000000..fdc1320d152 --- /dev/null +++ b/src/interfaces/libpq-oauth/README @@ -0,0 +1,43 @@ +libpq-oauth is an optional module implementing the Device Authorization flow for +OAuth clients (RFC 8628). It was originally developed as part of libpq core and +later split out as its own shared library in order to isolate its dependency on +libcurl. (End users who don't want the Curl dependency can simply choose not to +install this module.) + +If a connection string allows the use of OAuth, and the server asks for it, and +a libpq client has not installed its own custom OAuth flow, libpq will attempt +to delay-load this module using dlopen() and the following ABI. Failure to load +results in a failed connection. + += Load-Time ABI = + +This module ABI is an internal implementation detail, so it's subject to change +across releases; the name of the module (libpq-oauth-MAJOR-MINOR) reflects this. +The module exports the following symbols: + +- PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +- void pg_fe_cleanup_oauth_flow(PGconn *conn); + +pg_fe_run_oauth_flow and pg_fe_cleanup_oauth_flow are implementations of +conn->async_auth and conn->cleanup_async_auth, respectively. + +- void libpq_oauth_init(pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + +At the moment, pg_fe_run_oauth_flow() relies on libpq's pg_g_threadlock and +libpq_gettext(), which must be injected by libpq using this initialization +function before the flow is run. + +It also relies on libpq to expose conn->errorMessage, via the errmsg_impl. This +is done to decouple the module ABI from the offset of errorMessage, which can +change positions depending on configure-time options. This way we can safely +search the standard dlopen() paths (e.g. RPATH, LD_LIBRARY_PATH, the SO cache) +for an implementation module to use, even if that module wasn't compiled at the +same time as libpq. + += Static Build = + +The static library libpq.a does not perform any dynamic loading. If the builtin +flow is enabled, the application is expected to link against libpq-oauth.a +directly to provide the necessary symbols. diff --git a/src/interfaces/libpq-oauth/exports.txt b/src/interfaces/libpq-oauth/exports.txt new file mode 100644 index 00000000000..6891a83dbf9 --- /dev/null +++ b/src/interfaces/libpq-oauth/exports.txt @@ -0,0 +1,4 @@ +# src/interfaces/libpq-oauth/exports.txt +libpq_oauth_init 1 +pg_fe_run_oauth_flow 2 +pg_fe_cleanup_oauth_flow 3 diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build new file mode 100644 index 00000000000..d97f893178a --- /dev/null +++ b/src/interfaces/libpq-oauth/meson.build @@ -0,0 +1,45 @@ +# Copyright (c) 2022-2025, PostgreSQL Global Development Group + +if not oauth_flow_supported + subdir_done() +endif + +libpq_oauth_sources = files( + 'oauth-curl.c', +) + +# The shared library needs additional glue symbols. +libpq_oauth_so_sources = files( + 'oauth-utils.c', +) +libpq_oauth_so_c_args = ['-DUSE_DYNAMIC_OAUTH'] + +export_file = custom_target('libpq-oauth.exports', + kwargs: gen_export_kwargs, +) + +# port needs to be in include path due to pthread-win32.h +libpq_oauth_inc = include_directories('.', '../libpq', '../../port') + +libpq_oauth_st = static_library('libpq-oauth', + libpq_oauth_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_pch: pch_postgres_fe_h, + dependencies: [frontend_stlib_code, libpq_oauth_deps], + kwargs: default_lib_args, +) + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. +libpq_oauth_name = 'libpq-oauth-@0@-@1@'.format(pg_version_major, pg_version_minor) + +libpq_oauth_so = shared_module(libpq_oauth_name, + libpq_oauth_sources + libpq_oauth_so_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_args: libpq_so_c_args, + c_pch: pch_postgres_fe_h, + dependencies: [frontend_shlib_code, libpq, libpq_oauth_deps], + link_depends: export_file, + link_args: export_fmt.format(export_file.full_path()), + kwargs: default_lib_args, +) diff --git a/src/interfaces/libpq/fe-auth-oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c similarity index 97% rename from src/interfaces/libpq/fe-auth-oauth-curl.c rename to src/interfaces/libpq-oauth/oauth-curl.c index c195e00cd28..3239315d952 100644 --- a/src/interfaces/libpq/fe-auth-oauth-curl.c +++ b/src/interfaces/libpq-oauth/oauth-curl.c @@ -1,6 +1,6 @@ /*------------------------------------------------------------------------- * - * fe-auth-oauth-curl.c + * oauth-curl.c * The libcurl implementation of OAuth/OIDC authentication, using the * OAuth Device Authorization Grant (RFC 8628). * @@ -8,7 +8,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * src/interfaces/libpq/fe-auth-oauth-curl.c + * src/interfaces/libpq-oauth/oauth-curl.c * *------------------------------------------------------------------------- */ @@ -17,20 +17,25 @@ #include <curl/curl.h> #include <math.h> -#ifdef HAVE_SYS_EPOLL_H +#include <unistd.h> + +#if defined(HAVE_SYS_EPOLL_H) #include <sys/epoll.h> #include <sys/timerfd.h> -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) #include <sys/event.h> +#else +#error libpq-oauth is not supported on this platform #endif -#include <unistd.h> #include "common/jsonapi.h" #include "fe-auth.h" #include "fe-auth-oauth.h" -#include "libpq-int.h" #include "mb/pg_wchar.h" +#include "oauth-curl.h" +#ifdef USE_DYNAMIC_OAUTH +#include "oauth-utils.h" +#endif /* * It's generally prudent to set a maximum response size to buffer in memory, @@ -1110,7 +1115,7 @@ parse_access_token(struct async_ctx *actx, struct token *tok) static bool setup_multiplexer(struct async_ctx *actx) { -#ifdef HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct epoll_event ev = {.events = EPOLLIN}; actx->mux = epoll_create1(EPOLL_CLOEXEC); @@ -1134,8 +1139,7 @@ setup_multiplexer(struct async_ctx *actx) } return true; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) actx->mux = kqueue(); if (actx->mux < 0) { @@ -1158,10 +1162,9 @@ setup_multiplexer(struct async_ctx *actx) } return true; +#else +#error setup_multiplexer is not implemented on this platform #endif - - actx_error(actx, "libpq does not support the Device Authorization flow on this platform"); - return false; } /* @@ -1174,7 +1177,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, { struct async_ctx *actx = ctx; -#ifdef HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct epoll_event ev = {0}; int res; int op = EPOLL_CTL_ADD; @@ -1230,8 +1233,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, } return 0; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) struct kevent ev[2] = {0}; struct kevent ev_out[2]; struct timespec timeout = {0}; @@ -1312,10 +1314,9 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, } return 0; +#else +#error register_socket is not implemented on this platform #endif - - actx_error(actx, "libpq does not support multiplexer sockets on this platform"); - return -1; } /* @@ -1334,7 +1335,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, static bool set_timer(struct async_ctx *actx, long timeout) { -#if HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct itimerspec spec = {0}; if (timeout < 0) @@ -1363,8 +1364,7 @@ set_timer(struct async_ctx *actx, long timeout) } return true; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) struct kevent ev; #ifdef __NetBSD__ @@ -1419,10 +1419,9 @@ set_timer(struct async_ctx *actx, long timeout) } return true; +#else +#error set_timer is not implemented on this platform #endif - - actx_error(actx, "libpq does not support timers on this platform"); - return false; } /* @@ -1433,7 +1432,7 @@ set_timer(struct async_ctx *actx, long timeout) static int timer_expired(struct async_ctx *actx) { -#if HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct itimerspec spec = {0}; if (timerfd_gettime(actx->timerfd, &spec) < 0) @@ -1453,8 +1452,7 @@ timer_expired(struct async_ctx *actx) /* If the remaining time to expiration is zero, we're done. */ return (spec.it_value.tv_sec == 0 && spec.it_value.tv_nsec == 0); -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) int res; /* Is the timer queue ready? */ @@ -1466,10 +1464,9 @@ timer_expired(struct async_ctx *actx) } return (res > 0); +#else +#error timer_expired is not implemented on this platform #endif - - actx_error(actx, "libpq does not support timers on this platform"); - return -1; } /* @@ -2487,8 +2484,9 @@ prompt_user(struct async_ctx *actx, PGconn *conn) .verification_uri_complete = actx->authz.verification_uri_complete, .expires_in = actx->authz.expires_in, }; + PQauthDataHook_type hook = PQgetAuthDataHook(); - res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); + res = hook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); if (!res) { @@ -2635,6 +2633,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) { fe_oauth_state *state = conn->sasl_state; struct async_ctx *actx; + PQExpBuffer errbuf; if (!initialize_curl(conn)) return PGRES_POLLING_FAILED; @@ -2825,41 +2824,49 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) error_return: + /* + * For the dynamic module build, we can't safely rely on the offset of + * conn->errorMessage, since it depends on build options like USE_SSL et + * al. libpq gives us a translator function instead. + */ +#ifdef USE_DYNAMIC_OAUTH + errbuf = conn_errorMessage(conn); +#else + errbuf = &conn->errorMessage; +#endif + /* * Assemble the three parts of our error: context, body, and detail. See * also the documentation for struct async_ctx. */ if (actx->errctx) { - appendPQExpBufferStr(&conn->errorMessage, - libpq_gettext(actx->errctx)); - appendPQExpBufferStr(&conn->errorMessage, ": "); + appendPQExpBufferStr(errbuf, libpq_gettext(actx->errctx)); + appendPQExpBufferStr(errbuf, ": "); } if (PQExpBufferDataBroken(actx->errbuf)) - appendPQExpBufferStr(&conn->errorMessage, - libpq_gettext("out of memory")); + appendPQExpBufferStr(errbuf, libpq_gettext("out of memory")); else - appendPQExpBufferStr(&conn->errorMessage, actx->errbuf.data); + appendPQExpBufferStr(errbuf, actx->errbuf.data); if (actx->curl_err[0]) { size_t len; - appendPQExpBuffer(&conn->errorMessage, - " (libcurl: %s)", actx->curl_err); + appendPQExpBuffer(errbuf, " (libcurl: %s)", actx->curl_err); /* Sometimes libcurl adds a newline to the error buffer. :( */ - len = conn->errorMessage.len; - if (len >= 2 && conn->errorMessage.data[len - 2] == '\n') + len = errbuf->len; + if (len >= 2 && errbuf->data[len - 2] == '\n') { - conn->errorMessage.data[len - 2] = ')'; - conn->errorMessage.data[len - 1] = '\0'; - conn->errorMessage.len--; + errbuf->data[len - 2] = ')'; + errbuf->data[len - 1] = '\0'; + errbuf->len--; } } - appendPQExpBufferChar(&conn->errorMessage, '\n'); + appendPQExpBufferChar(errbuf, '\n'); return PGRES_POLLING_FAILED; } diff --git a/src/interfaces/libpq-oauth/oauth-curl.h b/src/interfaces/libpq-oauth/oauth-curl.h new file mode 100644 index 00000000000..248d0424ad0 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-curl.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * oauth-curl.h + * + * Definitions for OAuth Device Authorization module + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-curl.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_CURL_H +#define OAUTH_CURL_H + +#include "libpq-fe.h" + +/* Exported async-auth callbacks. */ +extern PGDLLEXPORT PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +extern PGDLLEXPORT void pg_fe_cleanup_oauth_flow(PGconn *conn); + +#endif /* OAUTH_CURL_H */ diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c new file mode 100644 index 00000000000..1f85a6b0479 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.c @@ -0,0 +1,202 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.c + * + * "Glue" helpers providing a copy of some internal APIs from libpq. At + * some point in the future, we might be able to deduplicate. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq-oauth/oauth-utils.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include <signal.h> + +#include "libpq-int.h" +#include "oauth-utils.h" + +#ifndef USE_DYNAMIC_OAUTH +#error oauth-utils.c is not supported in static builds +#endif + +static libpq_gettext_func libpq_gettext_impl; + +pgthreadlock_t pg_g_threadlock; +conn_errorMessage_func conn_errorMessage; + +/*- + * Initializes libpq-oauth by setting necessary callbacks. + * + * The current implementation relies on the following private implementation + * details of libpq: + * + * - pg_g_threadlock: protects libcurl initialization if the underlying Curl + * installation is not threadsafe + * + * - libpq_gettext: translates error messages using libpq's message domain + * + * - conn->errorMessage: holds translated errors for the connection. This is + * handled through a translation shim, which avoids either depending on the + * offset of the errorMessage in PGconn, or needing to export the variadic + * libpq_append_conn_error(). + */ +void +libpq_oauth_init(pgthreadlock_t threadlock_impl, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl) +{ + pg_g_threadlock = threadlock_impl; + libpq_gettext_impl = gettext_impl; + conn_errorMessage = errmsg_impl; +} + +/* + * Append a formatted string to the error message buffer of the given + * connection, after translating it. This is a copy of libpq's internal API. + */ +void +libpq_append_conn_error(PGconn *conn, const char *fmt,...) +{ + int save_errno = errno; + bool done; + va_list args; + PQExpBuffer errorMessage = conn_errorMessage(conn); + + Assert(fmt[strlen(fmt) - 1] != '\n'); + + if (PQExpBufferBroken(errorMessage)) + return; /* already failed */ + + /* Loop in case we have to retry after enlarging the buffer. */ + do + { + errno = save_errno; + va_start(args, fmt); + done = appendPQExpBufferVA(errorMessage, libpq_gettext(fmt), args); + va_end(args); + } while (!done); + + appendPQExpBufferChar(errorMessage, '\n'); +} + +#ifdef ENABLE_NLS + +/* + * A shim that defers to the actual libpq_gettext(). + */ +char * +libpq_gettext(const char *msgid) +{ + if (!libpq_gettext_impl) + { + /* + * Possible if the libpq build didn't enable NLS but the libpq-oauth + * build did. That's an odd mismatch, but we can handle it. + * + * Note that callers of libpq_gettext() have to treat the return value + * as if it were const, because builds without NLS simply pass through + * their argument. + */ + return unconstify(char *, msgid); + } + + return libpq_gettext_impl(msgid); +} + +#endif /* ENABLE_NLS */ + +/* + * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment. + */ +bool +oauth_unsafe_debugging_enabled(void) +{ + const char *env = getenv("PGOAUTHDEBUG"); + + return (env && strcmp(env, "UNSAFE") == 0); +} + +/* + * Duplicate SOCK_ERRNO* definitions from libpq-int.h, for use by + * pq_block/reset_sigpipe(). + */ +#ifdef WIN32 +#define SOCK_ERRNO (WSAGetLastError()) +#define SOCK_ERRNO_SET(e) WSASetLastError(e) +#else +#define SOCK_ERRNO errno +#define SOCK_ERRNO_SET(e) (errno = (e)) +#endif + +/* + * Block SIGPIPE for this thread. This is a copy of libpq's internal API. + */ +int +pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending) +{ + sigset_t sigpipe_sigset; + sigset_t sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + /* Block SIGPIPE and save previous mask for later reset */ + SOCK_ERRNO_SET(pthread_sigmask(SIG_BLOCK, &sigpipe_sigset, osigset)); + if (SOCK_ERRNO) + return -1; + + /* We can have a pending SIGPIPE only if it was blocked before */ + if (sigismember(osigset, SIGPIPE)) + { + /* Is there a pending SIGPIPE? */ + if (sigpending(&sigset) != 0) + return -1; + + if (sigismember(&sigset, SIGPIPE)) + *sigpipe_pending = true; + else + *sigpipe_pending = false; + } + else + *sigpipe_pending = false; + + return 0; +} + +/* + * Discard any pending SIGPIPE and reset the signal mask. This is a copy of + * libpq's internal API. + */ +void +pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe) +{ + int save_errno = SOCK_ERRNO; + int signo; + sigset_t sigset; + + /* Clear SIGPIPE only if none was pending */ + if (got_epipe && !sigpipe_pending) + { + if (sigpending(&sigset) == 0 && + sigismember(&sigset, SIGPIPE)) + { + sigset_t sigpipe_sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + sigwait(&sigpipe_sigset, &signo); + } + } + + /* Restore saved block mask */ + pthread_sigmask(SIG_SETMASK, osigset, NULL); + + SOCK_ERRNO_SET(save_errno); +} diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h new file mode 100644 index 00000000000..e2a9d01237d --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.h @@ -0,0 +1,38 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.h + * + * Definitions providing missing libpq internal APIs + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-utils.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_UTILS_H +#define OAUTH_UTILS_H + +#include "libpq-fe.h" +#include "pqexpbuffer.h" + +typedef char *(*libpq_gettext_func) (const char *msgid); +typedef PQExpBuffer (*conn_errorMessage_func) (PGconn *conn); + +/* Initializes libpq-oauth. */ +extern PGDLLEXPORT void libpq_oauth_init(pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + +/* Callback to safely obtain conn->errorMessage from a PGconn. */ +extern conn_errorMessage_func conn_errorMessage; + +/* Duplicated APIs, copied from libpq. */ +extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3); +extern bool oauth_unsafe_debugging_enabled(void); +extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending); +extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe); + +#endif /* OAUTH_UTILS_H */ diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 90b0b65db6f..c6fe5fec7f6 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -31,7 +31,6 @@ endif OBJS = \ $(WIN32RES) \ - fe-auth-oauth.o \ fe-auth-scram.o \ fe-cancel.o \ fe-connect.o \ @@ -64,9 +63,11 @@ OBJS += \ fe-secure-gssapi.o endif -ifeq ($(with_libcurl),yes) -OBJS += fe-auth-oauth-curl.o -endif +# The OAuth implementation differs depending on the type of library being built. +OBJS_STATIC = fe-auth-oauth.o + +fe-auth-oauth_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH +OBJS_SHLIB = fe-auth-oauth_shlib.o ifeq ($(PORTNAME), cygwin) override shlib = cyg$(NAME)$(DLSUFFIX) @@ -86,7 +87,7 @@ endif # that are built correctly for use in a shlib. SHLIB_LINK_INTERNAL = -lpgcommon_shlib -lpgport_shlib ifneq ($(PORTNAME), win32) -SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lcurl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) +SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) else SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi32 -lssl -lsocket -lnsl -lresolv -lintl -lm $(PTHREAD_LIBS), $(LIBS)) $(LDAP_LIBS_FE) endif @@ -101,12 +102,26 @@ ifeq ($(with_ssl),openssl) PKG_CONFIG_REQUIRES_PRIVATE = libssl, libcrypto endif +ifeq ($(with_libcurl),yes) +# libpq.so doesn't link against libcurl, but libpq.a needs libpq-oauth, and +# libpq-oauth needs libcurl. Put both into *.private. +PKG_CONFIG_REQUIRES_PRIVATE += libcurl +%.pc: override SHLIB_LINK_INTERNAL += -lpq-oauth +endif + all: all-lib libpq-refs-stamp # Shared library stuff include $(top_srcdir)/src/Makefile.shlib backend_src = $(top_srcdir)/src/backend +# Add shlib-/stlib-specific objects. +$(shlib): override OBJS += $(OBJS_SHLIB) +$(shlib): $(OBJS_SHLIB) + +$(stlib): override OBJS += $(OBJS_STATIC) +$(stlib): $(OBJS_STATIC) + # Check for functions that libpq must not call, currently just exit(). # (Ideally we'd reject abort() too, but there are various scenarios where # build toolchains insert abort() calls, e.g. to implement assert().) @@ -115,8 +130,6 @@ backend_src = $(top_srcdir)/src/backend # which seems to insert references to that even in pure C code. Excluding # __tsan_func_exit is necessary when using ThreadSanitizer data race detector # which use this function for instrumentation of function exit. -# libcurl registers an exit handler in the memory debugging code when running -# with LeakSanitizer. # Skip the test when profiling, as gcc may insert exit() calls for that. # Also skip the test on platforms where libpq infrastructure may be provided # by statically-linked libraries, as we can't expect them to honor this @@ -124,7 +137,7 @@ backend_src = $(top_srcdir)/src/backend libpq-refs-stamp: $(shlib) ifneq ($(enable_coverage), yes) ifeq (,$(filter solaris,$(PORTNAME))) - @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit -e _atexit | grep exit; then \ + @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit | grep exit; then \ echo 'libpq must not be calling any function which invokes exit'; exit 1; \ fi endif @@ -138,6 +151,11 @@ fe-misc.o: fe-misc.c $(top_builddir)/src/port/pg_config_paths.h $(top_builddir)/src/port/pg_config_paths.h: $(MAKE) -C $(top_builddir)/src/port pg_config_paths.h +# Use src/common/Makefile's trick for tracking dependencies of shlib-specific +# objects. +%_shlib.o: %.c %.o + $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@ + install: all installdirs install-lib $(INSTALL_DATA) $(srcdir)/libpq-fe.h '$(DESTDIR)$(includedir)' $(INSTALL_DATA) $(srcdir)/libpq-events.h '$(DESTDIR)$(includedir)' @@ -171,6 +189,6 @@ uninstall: uninstall-lib clean distclean: clean-lib $(MAKE) -C test $@ rm -rf tmp_check - rm -f $(OBJS) pthread.h libpq-refs-stamp + rm -f $(OBJS) $(OBJS_SHLIB) $(OBJS_STATIC) pthread.h libpq-refs-stamp # Might be left over from a Win32 client-only build rm -f pg_config_paths.h diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index d5143766858..0625cf39e9a 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -210,3 +210,4 @@ PQsetAuthDataHook 207 PQgetAuthDataHook 208 PQdefaultAuthDataHook 209 PQfullProtocolVersion 210 +appendPQExpBufferVA 211 diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c index cf1a25e2ccc..ccdd9139cf1 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,186 @@ 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); +typedef PQExpBuffer (*conn_errorMessage_func) (PGconn *conn); + +/* + * This shim is injected into libpq-oauth so that it doesn't depend on the + * offset of conn->errorMessage. + * + * TODO: look into exporting libpq_append_conn_error or a comparable API from + * libpq, instead. + */ +static PQExpBuffer +conn_errorMessage(PGconn *conn) +{ + return &conn->errorMessage; +} + +/* + * 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); + 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 "-" PG_MINORVERSION DLSUFFIX; +#else + "libpq-oauth-" PG_MAJORVERSION "-" PG_MINORVERSION 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); + + 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 +977,10 @@ setup_token_request(PGconn *conn, fe_oauth_state *state) libpq_append_conn_error(conn, "user-defined OAuth flow failed"); goto fail; } - else + else if (!use_builtin_flow(conn, state)) { -#if USE_LIBCURL - /* Hand off to our built-in OAuth flow. */ - conn->async_auth = pg_fe_run_oauth_flow; - conn->cleanup_async_auth = pg_fe_cleanup_oauth_flow; - -#else - libpq_append_conn_error(conn, "no custom OAuth flows are available, and libpq was not built with libcurl support"); + libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)"); goto fail; - -#endif } return true; diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h index 3f1a7503a01..687e664475f 100644 --- a/src/interfaces/libpq/fe-auth-oauth.h +++ b/src/interfaces/libpq/fe-auth-oauth.h @@ -33,12 +33,13 @@ typedef struct PGconn *conn; void *async_ctx; + + void *builtin_flow; } fe_oauth_state; -extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); -extern void pg_fe_cleanup_oauth_flow(PGconn *conn); extern void pqClearOAuthToken(PGconn *conn); extern bool oauth_unsafe_debugging_enabled(void); +extern bool use_builtin_flow(PGconn *conn, fe_oauth_state *state); /* Mechanisms in fe-auth-oauth.c */ extern const pg_fe_sasl_mech pg_oauth_mech; diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build index 292fecf3320..a74e885b169 100644 --- a/src/interfaces/libpq/meson.build +++ b/src/interfaces/libpq/meson.build @@ -38,10 +38,6 @@ if gssapi.found() ) endif -if libcurl.found() - libpq_sources += files('fe-auth-oauth-curl.c') -endif - export_file = custom_target('libpq.exports', kwargs: gen_export_kwargs, ) @@ -50,6 +46,9 @@ export_file = custom_target('libpq.exports', libpq_inc = include_directories('.', '../../port') libpq_c_args = ['-DSO_MAJOR_VERSION=5'] +# The OAuth implementation differs depending on the type of library being built. +libpq_so_c_args = ['-DUSE_DYNAMIC_OAUTH'] + # Not using both_libraries() here as # 1) resource files should only be in the shared library # 2) we want the .pc file to include a dependency to {pgport,common}_static for @@ -70,7 +69,7 @@ libpq_st = static_library('libpq', libpq_so = shared_library('libpq', libpq_sources + libpq_so_sources, include_directories: [libpq_inc, postgres_inc], - c_args: libpq_c_args, + c_args: libpq_c_args + libpq_so_c_args, c_pch: pch_postgres_fe_h, version: '5.' + pg_version_major.to_string(), soversion: host_system != 'windows' ? '5' : '', @@ -86,12 +85,26 @@ libpq = declare_dependency( include_directories: [include_directories('.')] ) +private_deps = [ + frontend_stlib_code, + libpq_deps, +] + +if oauth_flow_supported + # libpq.so doesn't link against libcurl, but libpq.a needs libpq-oauth, and + # libpq-oauth needs libcurl. Put both into *.private. + private_deps += [ + libpq_oauth_deps, + '-lpq-oauth', + ] +endif + pkgconfig.generate( name: 'libpq', description: 'PostgreSQL libpq library', url: pg_url, libraries: libpq, - libraries_private: [frontend_stlib_code, libpq_deps], + libraries_private: private_deps, ) install_headers( diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk index ae761265852..b87df277d93 100644 --- a/src/interfaces/libpq/nls.mk +++ b/src/interfaces/libpq/nls.mk @@ -13,15 +13,21 @@ GETTEXT_FILES = fe-auth.c \ fe-secure-common.c \ fe-secure-gssapi.c \ fe-secure-openssl.c \ - win32.c -GETTEXT_TRIGGERS = libpq_append_conn_error:2 \ + win32.c \ + ../libpq-oauth/oauth-curl.c \ + ../libpq-oauth/oauth-utils.c +GETTEXT_TRIGGERS = actx_error:2 \ + libpq_append_conn_error:2 \ libpq_append_error:2 \ libpq_gettext \ libpq_ngettext:1,2 \ + oauth_parse_set_error:2 \ pqInternalNotice:2 -GETTEXT_FLAGS = libpq_append_conn_error:2:c-format \ +GETTEXT_FLAGS = actx_error:2:c-format \ + libpq_append_conn_error:2:c-format \ libpq_append_error:2:c-format \ libpq_gettext:1:pass-c-format \ libpq_ngettext:1:pass-c-format \ libpq_ngettext:2:pass-c-format \ + oauth_parse_set_error:2:c-format \ pqInternalNotice:2:c-format diff --git a/src/makefiles/meson.build b/src/makefiles/meson.build index e3adb5d8dc4..48d01a54dc6 100644 --- a/src/makefiles/meson.build +++ b/src/makefiles/meson.build @@ -204,6 +204,8 @@ pgxs_empty = [ 'LIBNUMA_CFLAGS', 'LIBNUMA_LIBS', 'LIBURING_CFLAGS', 'LIBURING_LIBS', + + 'LIBCURL_CPPFLAGS', 'LIBCURL_LDFLAGS', 'LIBCURL_LDLIBS', ] if host_system == 'windows' and cc.get_argument_syntax() != 'msvc' diff --git a/src/test/modules/oauth_validator/meson.build b/src/test/modules/oauth_validator/meson.build index 36d1b26369f..e190f9cf15a 100644 --- a/src/test/modules/oauth_validator/meson.build +++ b/src/test/modules/oauth_validator/meson.build @@ -78,7 +78,7 @@ tests += { ], 'env': { 'PYTHON': python.path(), - 'with_libcurl': libcurl.found() ? 'yes' : 'no', + 'with_libcurl': oauth_flow_supported ? 'yes' : 'no', 'with_python': 'yes', }, }, diff --git a/src/test/modules/oauth_validator/t/002_client.pl b/src/test/modules/oauth_validator/t/002_client.pl index 8dd502f41e1..21d4acc1926 100644 --- a/src/test/modules/oauth_validator/t/002_client.pl +++ b/src/test/modules/oauth_validator/t/002_client.pl @@ -110,7 +110,7 @@ if ($ENV{with_libcurl} ne 'yes') "fails without custom hook installed", flags => ["--no-hook"], expected_stderr => - qr/no custom OAuth flows are available, and libpq was not built with libcurl support/ + qr/no OAuth flows are available \(try installing the libpq-oauth package\)/ ); } -- 2.34.1 ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-22 10:02 Daniel Gustafsson <[email protected]> parent: Jacob Champion <[email protected]> 0 siblings, 1 reply; 36+ messages in thread From: Daniel Gustafsson @ 2025-04-22 10:02 UTC (permalink / raw) To: Jacob Champion <[email protected]>; +Cc: Christoph Berg <[email protected]>; Jelte Fennema-Nio <[email protected]>; Peter Eisentraut <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]>; Devrim Gündüz <[email protected]> > On 22 Apr 2025, at 01:19, Jacob Champion <[email protected]> wrote: > v8 also makes the following changes: Thanks for this version, a few small comments: + if oauth_flow_supported + cdata.set('USE_LIBCURL', 1) + elif libcurlopt.enabled() + error('client OAuth is not supported on this platform') + endif We already know that libcurlopt.enabled() is true here so maybe just doing if-else-endif would make it more readable and save readers thinking it might have changed? Also, "client OAuth" reads a bit strange, how about "client-side OAuth" or "OAuth flow module"? - appendPQExpBufferStr(&conn->errorMessage, - libpq_gettext(actx->errctx)); - appendPQExpBufferStr(&conn->errorMessage, ": "); + appendPQExpBufferStr(errbuf, libpq_gettext(actx->errctx)); + appendPQExpBufferStr(errbuf, ": "); I think we should take this opportunity to turn this into a appendPQExpBuffer() with a format string instead of two calls. + len = errbuf->len; + if (len >= 2 && errbuf->data[len - 2] == '\n') Now that the actual variable, errbuf->len, is short and very descriptive I wonder if we shouldn't just use this as it makes the code even clearer IMO. -- Daniel Gustafsson ^ permalink raw reply [nested|flat] 36+ messages in thread
* Re: [PoC] Federated Authn/z with OAUTHBEARER @ 2025-04-22 23:41 Jacob Champion <[email protected]> parent: Daniel Gustafsson <[email protected]> 0 siblings, 0 replies; 36+ messages in thread From: Jacob Champion @ 2025-04-22 23:41 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: Christoph Berg <[email protected]>; Jelte Fennema-Nio <[email protected]>; Peter Eisentraut <[email protected]>; Andres Freund <[email protected]>; Tom Lane <[email protected]>; Bruce Momjian <[email protected]>; pgsql-hackers; Thomas Munro <[email protected]>; Nazir Bilal Yavuz <[email protected]>; Antonin Houska <[email protected]>; Wolfgang Walther <[email protected]>; Devrim Gündüz <[email protected]> On Tue, Apr 22, 2025 at 3:02 AM Daniel Gustafsson <[email protected]> wrote: > + if oauth_flow_supported > + cdata.set('USE_LIBCURL', 1) > + elif libcurlopt.enabled() > + error('client OAuth is not supported on this platform') > + endif > We already know that libcurlopt.enabled() is true here so maybe just doing > if-else-endif would make it more readable and save readers thinking it might > have changed? Features are tri-state, so libcurlopt.disabled() and libcurlopt.enabled() can both be false. :( My intent is to fall through nicely in the case where -Dlibcurl=auto. (Our minimum version of Meson is too old to switch to syntax that makes this more readable, like .allowed(), .require(), .disable_if(), etc...) > Also, "client OAuth" reads a bit strange, how about "client-side > OAuth" or "OAuth flow module"? > ... > I think we should take this opportunity to turn this into a appendPQExpBuffer() > with a format string instead of two calls. > ... > Now that the actual variable, errbuf->len, is short and very descriptive I > wonder if we shouldn't just use this as it makes the code even clearer IMO. All three done in v9, attached. Thanks! --Jacob 1: 5f87f11b18e = 1: 5f87f11b18e Add minor-version counterpart to (PG_)MAJORVERSION 2: 4c9cc7f69af ! 2: 9e37fd7c217 oauth: Move the builtin flow into a separate module @@ configure.ac: if test "$PORTNAME" = "win32" ; then +if test "$with_libcurl" = yes ; then + # Error out early if this platform can't support libpq-oauth. + if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then -+ AC_MSG_ERROR([client OAuth is not supported on this platform]) ++ AC_MSG_ERROR([client-side OAuth is not supported on this platform]) + fi +fi + @@ meson.build: if not libcurlopt.disabled() + if oauth_flow_supported + cdata.set('USE_LIBCURL', 1) + elif libcurlopt.enabled() -+ error('client OAuth is not supported on this platform') ++ error('client-side OAuth is not supported on this platform') + endif + else @@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn) * also the documentation for struct async_ctx. */ if (actx->errctx) - { +- { - appendPQExpBufferStr(&conn->errorMessage, - libpq_gettext(actx->errctx)); - appendPQExpBufferStr(&conn->errorMessage, ": "); -+ appendPQExpBufferStr(errbuf, libpq_gettext(actx->errctx)); -+ appendPQExpBufferStr(errbuf, ": "); - } +- } ++ appendPQExpBuffer(errbuf, "%s: ", libpq_gettext(actx->errctx)); if (PQExpBufferDataBroken(actx->errbuf)) - appendPQExpBufferStr(&conn->errorMessage, @@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn) if (actx->curl_err[0]) { - size_t len; - +- size_t len; +- - appendPQExpBuffer(&conn->errorMessage, - " (libcurl: %s)", actx->curl_err); + appendPQExpBuffer(errbuf, " (libcurl: %s)", actx->curl_err); @@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn) /* Sometimes libcurl adds a newline to the error buffer. :( */ - len = conn->errorMessage.len; - if (len >= 2 && conn->errorMessage.data[len - 2] == '\n') -+ len = errbuf->len; -+ if (len >= 2 && errbuf->data[len - 2] == '\n') ++ if (errbuf->len >= 2 && errbuf->data[errbuf->len - 2] == '\n') { - conn->errorMessage.data[len - 2] = ')'; - conn->errorMessage.data[len - 1] = '\0'; - conn->errorMessage.len--; -+ errbuf->data[len - 2] = ')'; -+ errbuf->data[len - 1] = '\0'; ++ errbuf->data[errbuf->len - 2] = ')'; ++ errbuf->data[errbuf->len - 1] = '\0'; + errbuf->len--; } } Attachments: [text/plain] since-v8.diff.txt (2.7K, ../../CAOYmi+=ka9dTDtFhHjnL7jLd-rA1Q+VuU6=vjMM=jjm6_yCrpg@mail.gmail.com/2-since-v8.diff.txt) download | inline: 1: 5f87f11b18e = 1: 5f87f11b18e Add minor-version counterpart to (PG_)MAJORVERSION 2: 4c9cc7f69af ! 2: 9e37fd7c217 oauth: Move the builtin flow into a separate module @@ configure.ac: if test "$PORTNAME" = "win32" ; then +if test "$with_libcurl" = yes ; then + # Error out early if this platform can't support libpq-oauth. + if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then -+ AC_MSG_ERROR([client OAuth is not supported on this platform]) ++ AC_MSG_ERROR([client-side OAuth is not supported on this platform]) + fi +fi + @@ meson.build: if not libcurlopt.disabled() + if oauth_flow_supported + cdata.set('USE_LIBCURL', 1) + elif libcurlopt.enabled() -+ error('client OAuth is not supported on this platform') ++ error('client-side OAuth is not supported on this platform') + endif + else @@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn) * also the documentation for struct async_ctx. */ if (actx->errctx) - { +- { - appendPQExpBufferStr(&conn->errorMessage, - libpq_gettext(actx->errctx)); - appendPQExpBufferStr(&conn->errorMessage, ": "); -+ appendPQExpBufferStr(errbuf, libpq_gettext(actx->errctx)); -+ appendPQExpBufferStr(errbuf, ": "); - } +- } ++ appendPQExpBuffer(errbuf, "%s: ", libpq_gettext(actx->errctx)); if (PQExpBufferDataBroken(actx->errbuf)) - appendPQExpBufferStr(&conn->errorMessage, @@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn) if (actx->curl_err[0]) { - size_t len; - +- size_t len; +- - appendPQExpBuffer(&conn->errorMessage, - " (libcurl: %s)", actx->curl_err); + appendPQExpBuffer(errbuf, " (libcurl: %s)", actx->curl_err); @@ src/interfaces/libpq-oauth/oauth-curl.c: pg_fe_run_oauth_flow_impl(PGconn *conn) /* Sometimes libcurl adds a newline to the error buffer. :( */ - len = conn->errorMessage.len; - if (len >= 2 && conn->errorMessage.data[len - 2] == '\n') -+ len = errbuf->len; -+ if (len >= 2 && errbuf->data[len - 2] == '\n') ++ if (errbuf->len >= 2 && errbuf->data[errbuf->len - 2] == '\n') { - conn->errorMessage.data[len - 2] = ')'; - conn->errorMessage.data[len - 1] = '\0'; - conn->errorMessage.len--; -+ errbuf->data[len - 2] = ')'; -+ errbuf->data[len - 1] = '\0'; ++ errbuf->data[errbuf->len - 2] = ')'; ++ errbuf->data[errbuf->len - 1] = '\0'; + errbuf->len--; } } [application/octet-stream] v9-0001-Add-minor-version-counterpart-to-PG_-MAJORVERSION.patch (3.5K, ../../CAOYmi+=ka9dTDtFhHjnL7jLd-rA1Q+VuU6=vjMM=jjm6_yCrpg@mail.gmail.com/3-v9-0001-Add-minor-version-counterpart-to-PG_-MAJORVERSION.patch) download | inline diff: From 5f87f11b18ea83615c342c832caace49bf7e3897 Mon Sep 17 00:00:00 2001 From: Jacob Champion <[email protected]> Date: Mon, 21 Apr 2025 13:43:08 -0700 Subject: [PATCH v9 1/2] Add minor-version counterpart to (PG_)MAJORVERSION An upcoming commit will name a library, libpq-oauth, using the major and minor versions. Make the minor version accessible from the Makefiles and as a string constant in the code. --- configure | 7 +++++++ configure.ac | 2 ++ meson.build | 1 + src/Makefile.global.in | 1 + src/include/pg_config.h.in | 3 +++ src/makefiles/meson.build | 1 + 6 files changed, 15 insertions(+) diff --git a/configure b/configure index 0936010718d..3d783793dfa 100755 --- a/configure +++ b/configure @@ -792,6 +792,7 @@ build_os build_vendor build_cpu build +PG_MINORVERSION PG_MAJORVERSION target_alias host_alias @@ -2877,6 +2878,12 @@ cat >>confdefs.h <<_ACEOF _ACEOF + +cat >>confdefs.h <<_ACEOF +#define PG_MINORVERSION "$PG_MINORVERSION" +_ACEOF + + cat >>confdefs.h <<_ACEOF #define PG_MINORVERSION_NUM $PG_MINORVERSION _ACEOF diff --git a/configure.ac b/configure.ac index 2a78cddd825..1cb3a0ff042 100644 --- a/configure.ac +++ b/configure.ac @@ -35,6 +35,8 @@ test -n "$PG_MINORVERSION" || PG_MINORVERSION=0 AC_SUBST(PG_MAJORVERSION) AC_DEFINE_UNQUOTED(PG_MAJORVERSION, "$PG_MAJORVERSION", [PostgreSQL major version as a string]) AC_DEFINE_UNQUOTED(PG_MAJORVERSION_NUM, $PG_MAJORVERSION, [PostgreSQL major version number]) +AC_SUBST(PG_MINORVERSION) +AC_DEFINE_UNQUOTED(PG_MINORVERSION, "$PG_MINORVERSION", [PostgreSQL minor version as a string]) AC_DEFINE_UNQUOTED(PG_MINORVERSION_NUM, $PG_MINORVERSION, [PostgreSQL minor version number]) PGAC_ARG_REQ(with, extra-version, [STRING], [append STRING to version], diff --git a/meson.build b/meson.build index a1516e54529..18423a7c13e 100644 --- a/meson.build +++ b/meson.build @@ -148,6 +148,7 @@ pg_version += get_option('extra_version') cdata.set_quoted('PG_VERSION', pg_version) cdata.set_quoted('PG_MAJORVERSION', pg_version_major.to_string()) cdata.set('PG_MAJORVERSION_NUM', pg_version_major) +cdata.set_quoted('PG_MINORVERSION', pg_version_minor.to_string()) cdata.set('PG_MINORVERSION_NUM', pg_version_minor) cdata.set('PG_VERSION_NUM', pg_version_num) # PG_VERSION_STR is built later, it depends on compiler test results diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 6722fbdf365..54b4a07712e 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -40,6 +40,7 @@ maintainer-clean: distclean # PostgreSQL version number VERSION = @PACKAGE_VERSION@ MAJORVERSION = @PG_MAJORVERSION@ +MINORVERSION = @PG_MINORVERSION@ VERSION_NUM = @PG_VERSION_NUM@ PACKAGE_URL = @PACKAGE_URL@ diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in index c3cc9fa856d..4fe37d228c5 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -602,6 +602,9 @@ /* PostgreSQL major version number */ #undef PG_MAJORVERSION_NUM +/* PostgreSQL minor version as a string */ +#undef PG_MINORVERSION + /* PostgreSQL minor version number */ #undef PG_MINORVERSION_NUM diff --git a/src/makefiles/meson.build b/src/makefiles/meson.build index 55da678ec27..e3adb5d8dc4 100644 --- a/src/makefiles/meson.build +++ b/src/makefiles/meson.build @@ -36,6 +36,7 @@ pgxs_kv = { 'PACKAGE_URL': pg_url, 'PACKAGE_VERSION': pg_version, 'PG_MAJORVERSION': pg_version_major, + 'PG_MINORVERSION': pg_version_minor, 'PG_VERSION_NUM': pg_version_num, 'configure_input': 'meson', -- 2.34.1 [application/octet-stream] v9-0002-oauth-Move-the-builtin-flow-into-a-separate-modul.patch (56.7K, ../../CAOYmi+=ka9dTDtFhHjnL7jLd-rA1Q+VuU6=vjMM=jjm6_yCrpg@mail.gmail.com/4-v9-0002-oauth-Move-the-builtin-flow-into-a-separate-modul.patch) download | inline diff: From 9e37fd7c2171d8fe1880c0b00625b7ca6a833062 Mon Sep 17 00:00:00 2001 From: Jacob Champion <[email protected]> Date: Wed, 26 Mar 2025 10:55:28 -0700 Subject: [PATCH v9 2/2] 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 offset of conn->errorMessage to libpq-oauth, so that we can function without crashing if the module on the search path came from a different build of Postgres. This ABI is considered "private". The module has no SONAME or version symlinks, and it's named libpq-oauth-<major>-<minor>.so to avoid mixing and matching across Postgres versions, in case internal struct order needs to change. (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 many people reviewed the design and implementation; thank you! Co-authored-by: Daniel Gustafsson <[email protected]> Reviewed-by: Andres Freund <[email protected]> Reviewed-by: Christoph Berg <[email protected]> Reviewed-by: Jelte Fennema-Nio <[email protected]> Reviewed-by: Peter Eisentraut <[email protected]> Reviewed-by: Wolfgang Walther <[email protected]> Discussion: https://postgr.es/m/641687.1742360249%40sss.pgh.pa.us --- config/programs.m4 | 17 +- configure | 50 ++++- configure.ac | 26 ++- doc/src/sgml/installation.sgml | 8 + doc/src/sgml/libpq.sgml | 30 ++- meson.build | 32 ++- src/Makefile.global.in | 3 + src/interfaces/Makefile | 12 ++ src/interfaces/libpq-oauth/Makefile | 83 +++++++ src/interfaces/libpq-oauth/README | 43 ++++ src/interfaces/libpq-oauth/exports.txt | 4 + src/interfaces/libpq-oauth/meson.build | 45 ++++ .../oauth-curl.c} | 101 ++++----- src/interfaces/libpq-oauth/oauth-curl.h | 24 +++ src/interfaces/libpq-oauth/oauth-utils.c | 202 ++++++++++++++++++ src/interfaces/libpq-oauth/oauth-utils.h | 38 ++++ src/interfaces/libpq/Makefile | 36 +++- src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-auth-oauth.c | 197 ++++++++++++++++- src/interfaces/libpq/fe-auth-oauth.h | 5 +- src/interfaces/libpq/meson.build | 25 ++- src/interfaces/libpq/nls.mk | 12 +- src/makefiles/meson.build | 2 + src/test/modules/oauth_validator/meson.build | 2 +- .../modules/oauth_validator/t/002_client.pl | 2 +- 25 files changed, 884 insertions(+), 116 deletions(-) create mode 100644 src/interfaces/libpq-oauth/Makefile create mode 100644 src/interfaces/libpq-oauth/README create mode 100644 src/interfaces/libpq-oauth/exports.txt create mode 100644 src/interfaces/libpq-oauth/meson.build rename src/interfaces/{libpq/fe-auth-oauth-curl.c => libpq-oauth/oauth-curl.c} (97%) create mode 100644 src/interfaces/libpq-oauth/oauth-curl.h create mode 100644 src/interfaces/libpq-oauth/oauth-utils.c create mode 100644 src/interfaces/libpq-oauth/oauth-utils.h diff --git a/config/programs.m4 b/config/programs.m4 index 0a07feb37cc..0ad1e58b48d 100644 --- a/config/programs.m4 +++ b/config/programs.m4 @@ -286,9 +286,20 @@ AC_DEFUN([PGAC_CHECK_LIBCURL], [ AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file <curl/curl.h> is required for --with-libcurl])]) - AC_CHECK_LIB(curl, curl_multi_init, [], + AC_CHECK_LIB(curl, curl_multi_init, [ + AC_DEFINE([HAVE_LIBCURL], [1], [Define to 1 if you have the `curl' library (-lcurl).]) + AC_SUBST(LIBCURL_LDLIBS, -lcurl) + ], [AC_MSG_ERROR([library 'curl' does not provide curl_multi_init])]) + pgac_save_CPPFLAGS=$CPPFLAGS + pgac_save_LDFLAGS=$LDFLAGS + pgac_save_LIBS=$LIBS + + CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" + LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS" + LIBS="$LIBCURL_LDLIBS $LIBS" + # Check to see whether the current platform supports threadsafe Curl # initialization. AC_CACHE_CHECK([for curl_global_init thread safety], [pgac_cv__libcurl_threadsafe_init], @@ -338,4 +349,8 @@ AC_DEFUN([PGAC_CHECK_LIBCURL], *** lookups. Rebuild libcurl with the AsynchDNS feature enabled in order *** to use it with libpq.]) fi + + CPPFLAGS=$pgac_save_CPPFLAGS + LDFLAGS=$pgac_save_LDFLAGS + LIBS=$pgac_save_LIBS ])# PGAC_CHECK_LIBCURL diff --git a/configure b/configure index 3d783793dfa..eedd18e6d9a 100755 --- a/configure +++ b/configure @@ -655,6 +655,7 @@ UUID_LIBS LDAP_LIBS_BE LDAP_LIBS_FE with_ssl +LIBCURL_LDLIBS PTHREAD_CFLAGS PTHREAD_LIBS PTHREAD_CC @@ -711,6 +712,8 @@ with_libxml LIBNUMA_LIBS LIBNUMA_CFLAGS with_libnuma +LIBCURL_LDFLAGS +LIBCURL_CPPFLAGS LIBCURL_LIBS LIBCURL_CFLAGS with_libcurl @@ -9060,19 +9063,27 @@ $as_echo "yes" >&6; } fi - # We only care about -I, -D, and -L switches; - # note that -lcurl will be added by PGAC_CHECK_LIBCURL below. + # Curl's flags are kept separate from the standard CPPFLAGS/LDFLAGS. We use + # them only for libpq-oauth. + LIBCURL_CPPFLAGS= + LIBCURL_LDFLAGS= + + # We only care about -I, -D, and -L switches. Note that -lcurl will be added + # to LIBCURL_LDLIBS by PGAC_CHECK_LIBCURL, below. for pgac_option in $LIBCURL_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) LIBCURL_CPPFLAGS="$LIBCURL_CPPFLAGS $pgac_option";; esac done for pgac_option in $LIBCURL_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBCURL_LDFLAGS="$LIBCURL_LDFLAGS $pgac_option";; esac done + + + # OAuth requires python for testing if test "$with_python" != yes; then { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: *** OAuth support tests require --with-python to run" >&5 @@ -12711,9 +12722,6 @@ fi fi -# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults -# during gss_acquire_cred(). This is possibly related to Curl's Heimdal -# dependency on that platform? if test "$with_libcurl" = yes ; then ac_fn_c_check_header_mongrel "$LINENO" "curl/curl.h" "ac_cv_header_curl_curl_h" "$ac_includes_default" @@ -12761,17 +12769,26 @@ fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curl_curl_multi_init" >&5 $as_echo "$ac_cv_lib_curl_curl_multi_init" >&6; } if test "x$ac_cv_lib_curl_curl_multi_init" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LIBCURL 1 -_ACEOF - LIBS="-lcurl $LIBS" + +$as_echo "#define HAVE_LIBCURL 1" >>confdefs.h + + LIBCURL_LDLIBS=-lcurl + else as_fn_error $? "library 'curl' does not provide curl_multi_init" "$LINENO" 5 fi + pgac_save_CPPFLAGS=$CPPFLAGS + pgac_save_LDFLAGS=$LDFLAGS + pgac_save_LIBS=$LIBS + + CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS" + LDFLAGS="$LIBCURL_LDFLAGS $LDFLAGS" + LIBS="$LIBCURL_LDLIBS $LIBS" + # Check to see whether the current platform supports threadsafe Curl # initialization. { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_global_init thread safety" >&5 @@ -12875,6 +12892,10 @@ $as_echo "$pgac_cv__libcurl_async_dns" >&6; } *** to use it with libpq." "$LINENO" 5 fi + CPPFLAGS=$pgac_save_CPPFLAGS + LDFLAGS=$pgac_save_LDFLAGS + LIBS=$pgac_save_LIBS + fi if test "$with_gssapi" = yes ; then @@ -14523,6 +14544,13 @@ done fi +if test "$with_libcurl" = yes ; then + # Error out early if this platform can't support libpq-oauth. + if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then + as_fn_error $? "client OAuth is not supported on this platform" "$LINENO" 5 + fi +fi + ## ## Types, structures, compiler characteristics ## diff --git a/configure.ac b/configure.ac index 1cb3a0ff042..d1c8dd536cd 100644 --- a/configure.ac +++ b/configure.ac @@ -1035,19 +1035,27 @@ if test "$with_libcurl" = yes ; then # to explicitly set TLS 1.3 ciphersuites). PKG_CHECK_MODULES(LIBCURL, [libcurl >= 7.61.0]) - # We only care about -I, -D, and -L switches; - # note that -lcurl will be added by PGAC_CHECK_LIBCURL below. + # Curl's flags are kept separate from the standard CPPFLAGS/LDFLAGS. We use + # them only for libpq-oauth. + LIBCURL_CPPFLAGS= + LIBCURL_LDFLAGS= + + # We only care about -I, -D, and -L switches. Note that -lcurl will be added + # to LIBCURL_LDLIBS by PGAC_CHECK_LIBCURL, below. for pgac_option in $LIBCURL_CFLAGS; do case $pgac_option in - -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";; + -I*|-D*) LIBCURL_CPPFLAGS="$LIBCURL_CPPFLAGS $pgac_option";; esac done for pgac_option in $LIBCURL_LIBS; do case $pgac_option in - -L*) LDFLAGS="$LDFLAGS $pgac_option";; + -L*) LIBCURL_LDFLAGS="$LIBCURL_LDFLAGS $pgac_option";; esac done + AC_SUBST(LIBCURL_CPPFLAGS) + AC_SUBST(LIBCURL_LDFLAGS) + # OAuth requires python for testing if test "$with_python" != yes; then AC_MSG_WARN([*** OAuth support tests require --with-python to run]) @@ -1356,9 +1364,6 @@ failure. It is possible the compiler isn't looking in the proper directory. Use --without-zlib to disable zlib support.])]) fi -# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults -# during gss_acquire_cred(). This is possibly related to Curl's Heimdal -# dependency on that platform? if test "$with_libcurl" = yes ; then PGAC_CHECK_LIBCURL fi @@ -1656,6 +1661,13 @@ if test "$PORTNAME" = "win32" ; then AC_CHECK_HEADERS(crtdefs.h) fi +if test "$with_libcurl" = yes ; then + # Error out early if this platform can't support libpq-oauth. + if test "$ac_cv_header_sys_event_h" != yes -a "$ac_cv_header_sys_epoll_h" != yes; then + AC_MSG_ERROR([client-side OAuth is not supported on this platform]) + fi +fi + ## ## Types, structures, compiler characteristics ## diff --git a/doc/src/sgml/installation.sgml b/doc/src/sgml/installation.sgml index 077bcc20759..d928b103d22 100644 --- a/doc/src/sgml/installation.sgml +++ b/doc/src/sgml/installation.sgml @@ -313,6 +313,14 @@ </para> </listitem> + <listitem> + <para> + You need <productname>Curl</productname> to build an optional module + which implements the <link linkend="libpq-oauth">OAuth Device + Authorization flow</link> for client applications. + </para> + </listitem> + <listitem> <para> You need <productname>LZ4</productname>, if you want to support diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 3be66789ba7..cd748902f4d 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -10226,15 +10226,20 @@ void PQinitSSL(int do_ssl); <title>OAuth Support</title> <para> - libpq implements support for the OAuth v2 Device Authorization client flow, + <application>libpq</application> implements support for the OAuth v2 Device Authorization client flow, documented in <ulink url="https://datatracker.ietf.org/doc/html/rfc8628">RFC 8628</ulink>, - which it will attempt to use by default if the server + as an optional module. See the <link linkend="configure-option-with-libcurl"> + installation documentation</link> for information on how to enable support + for Device Authorization as a builtin flow. + </para> + <para> + When support is enabled and the optional module installed, <application>libpq</application> + will use the builtin flow by default if the server <link linkend="auth-oauth">requests a bearer token</link> during authentication. This flow can be utilized even if the system running the client application does not have a usable web browser, for example when - running a client via <application>SSH</application>. Client applications may implement their own flows - instead; see <xref linkend="libpq-oauth-authdata-hooks"/>. + running a client via <acronym>SSH</acronym>. </para> <para> The builtin flow will, by default, print a URL to visit and a user code to @@ -10251,6 +10256,11 @@ Visit https://example.com/device and enter the code: ABCD-EFGH they match expectations, before continuing. Permissions should not be given to untrusted third parties. </para> + <para> + Client applications may implement their own flows to customize interaction + and integration with applications. See <xref linkend="libpq-oauth-authdata-hooks"/> + for more information on how add a custom flow to <application>libpq</application>. + </para> <para> For an OAuth client flow to be usable, the connection string must at minimum contain <xref linkend="libpq-connect-oauth-issuer"/> and @@ -10366,7 +10376,9 @@ typedef struct _PGpromptOAuthDevice </synopsis> </para> <para> - The OAuth Device Authorization flow included in <application>libpq</application> + The OAuth Device Authorization flow which + <link linkend="configure-option-with-libcurl">can be included</link> + in <application>libpq</application> requires the end user to visit a URL with a browser, then enter a code which permits <application>libpq</application> to connect to the server on their behalf. The default prompt simply prints the @@ -10378,7 +10390,8 @@ typedef struct _PGpromptOAuthDevice This callback is only invoked during the builtin device authorization flow. If the application installs a <link linkend="libpq-oauth-authdata-oauth-bearer-token">custom OAuth - flow</link>, this authdata type will not be used. + flow</link>, or <application>libpq</application> was not built with + support for the builtin flow, this authdata type will not be used. </para> <para> If a non-NULL <structfield>verification_uri_complete</structfield> is @@ -10400,8 +10413,9 @@ typedef struct _PGpromptOAuthDevice </term> <listitem> <para> - Replaces the entire OAuth flow with a custom implementation. The hook - should either directly return a Bearer token for the current + Adds a custom implementation of a flow, replacing the builtin flow if + it is <link linkend="configure-option-with-libcurl">installed</link>. + The hook should either directly return a Bearer token for the current user/issuer/scope combination, if one is available without blocking, or else set up an asynchronous callback to retrieve one. </para> diff --git a/meson.build b/meson.build index 18423a7c13e..2798922c6f0 100644 --- a/meson.build +++ b/meson.build @@ -107,6 +107,7 @@ os_deps = [] backend_both_deps = [] backend_deps = [] libpq_deps = [] +libpq_oauth_deps = [] pg_sysroot = '' @@ -861,13 +862,13 @@ endif ############################################################### libcurlopt = get_option('libcurl') +oauth_flow_supported = false + if not libcurlopt.disabled() # Check for libcurl 7.61.0 or higher (corresponding to RHEL8 and the ability # to explicitly set TLS 1.3 ciphersuites). libcurl = dependency('libcurl', version: '>= 7.61.0', required: libcurlopt) if libcurl.found() - cdata.set('USE_LIBCURL', 1) - # Check to see whether the current platform supports thread-safe Curl # initialization. libcurl_threadsafe_init = false @@ -939,6 +940,22 @@ if not libcurlopt.disabled() endif endif + # Check that the current platform supports our builtin flow. This requires + # libcurl and one of either epoll or kqueue. + oauth_flow_supported = ( + libcurl.found() + and (cc.check_header('sys/event.h', required: false, + args: test_c_args, include_directories: postgres_inc) + or cc.check_header('sys/epoll.h', required: false, + args: test_c_args, include_directories: postgres_inc)) + ) + + if oauth_flow_supported + cdata.set('USE_LIBCURL', 1) + elif libcurlopt.enabled() + error('client-side OAuth is not supported on this platform') + endif + else libcurl = not_found_dep endif @@ -3273,17 +3290,18 @@ libpq_deps += [ gssapi, ldap_r, - # XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults - # during gss_acquire_cred(). This is possibly related to Curl's Heimdal - # dependency on that platform? - libcurl, libintl, ssl, ] +libpq_oauth_deps += [ + libcurl, +] + subdir('src/interfaces/libpq') -# fe_utils depends on libpq +# fe_utils and libpq-oauth depends on libpq subdir('src/fe_utils') +subdir('src/interfaces/libpq-oauth') # for frontend binaries frontend_code = declare_dependency( diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 54b4a07712e..f4caece04df 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -348,6 +348,9 @@ perl_embed_ldflags = @perl_embed_ldflags@ AWK = @AWK@ LN_S = @LN_S@ +LIBCURL_CPPFLAGS = @LIBCURL_CPPFLAGS@ +LIBCURL_LDFLAGS = @LIBCURL_LDFLAGS@ +LIBCURL_LDLIBS = @LIBCURL_LDLIBS@ MSGFMT = @MSGFMT@ MSGFMT_FLAGS = @MSGFMT_FLAGS@ MSGMERGE = @MSGMERGE@ diff --git a/src/interfaces/Makefile b/src/interfaces/Makefile index 7d56b29d28f..e6822caa206 100644 --- a/src/interfaces/Makefile +++ b/src/interfaces/Makefile @@ -14,7 +14,19 @@ include $(top_builddir)/src/Makefile.global SUBDIRS = libpq ecpg +ifeq ($(with_libcurl), yes) +SUBDIRS += libpq-oauth +else +ALWAYS_SUBDIRS += libpq-oauth +endif + $(recurse) +$(recurse_always) all-ecpg-recurse: all-libpq-recurse install-ecpg-recurse: install-libpq-recurse + +ifeq ($(with_libcurl), yes) +all-libpq-oauth-recurse: all-libpq-recurse +install-libpq-oauth-recurse: install-libpq-recurse +endif diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile new file mode 100644 index 00000000000..98acaff1a3b --- /dev/null +++ b/src/interfaces/libpq-oauth/Makefile @@ -0,0 +1,83 @@ +#------------------------------------------------------------------------- +# +# Makefile for libpq-oauth +# +# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1994, Regents of the University of California +# +# src/interfaces/libpq-oauth/Makefile +# +#------------------------------------------------------------------------- + +subdir = src/interfaces/libpq-oauth +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +PGFILEDESC = "libpq-oauth - device authorization OAuth support" + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. +NAME = pq-oauth-$(MAJORVERSION)-$(MINORVERSION) + +# Force the name "libpq-oauth" for both the static and shared libraries. The +# staticlib doesn't need version information in its name. +override shlib := lib$(NAME)$(DLSUFFIX) +override stlib := libpq-oauth.a + +override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(LIBCURL_CPPFLAGS) $(CPPFLAGS) + +OBJS = \ + $(WIN32RES) + +OBJS_STATIC = oauth-curl.o + +# The shared library needs additional glue symbols. +OBJS_SHLIB = \ + oauth-curl_shlib.o \ + oauth-utils.o \ + +oauth-utils.o: override CPPFLAGS += -DUSE_DYNAMIC_OAUTH +oauth-curl_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH + +# Add shlib-/stlib-specific objects. +$(shlib): override OBJS += $(OBJS_SHLIB) +$(shlib): $(OBJS_SHLIB) + +$(stlib): override OBJS += $(OBJS_STATIC) +$(stlib): $(OBJS_STATIC) + +SHLIB_LINK_INTERNAL = $(libpq_pgport_shlib) +SHLIB_LINK = $(LIBCURL_LDFLAGS) $(LIBCURL_LDLIBS) +SHLIB_PREREQS = submake-libpq +SHLIB_EXPORTS = exports.txt + +# Disable -bundle_loader on macOS. +BE_DLLLIBS = + +# By default, a library without an SONAME doesn't get a static library, so we +# add it to the build explicitly. +all: all-lib all-static-lib + +# Shared library stuff +include $(top_srcdir)/src/Makefile.shlib + +# Use src/common/Makefile's trick for tracking dependencies of shlib-specific +# objects. +%_shlib.o: %.c %.o + $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@ + +# Ignore the standard rules for SONAME-less installation; we want both the +# static and shared libraries to go into libdir. +install: all installdirs $(stlib) $(shlib) + $(INSTALL_SHLIB) $(shlib) '$(DESTDIR)$(libdir)/$(shlib)' + $(INSTALL_STLIB) $(stlib) '$(DESTDIR)$(libdir)/$(stlib)' + +installdirs: + $(MKDIR_P) '$(DESTDIR)$(libdir)' + +uninstall: + rm -f '$(DESTDIR)$(libdir)/$(stlib)' + rm -f '$(DESTDIR)$(libdir)/$(shlib)' + +clean distclean: clean-lib + rm -f $(OBJS) $(OBJS_STATIC) $(OBJS_SHLIB) diff --git a/src/interfaces/libpq-oauth/README b/src/interfaces/libpq-oauth/README new file mode 100644 index 00000000000..fdc1320d152 --- /dev/null +++ b/src/interfaces/libpq-oauth/README @@ -0,0 +1,43 @@ +libpq-oauth is an optional module implementing the Device Authorization flow for +OAuth clients (RFC 8628). It was originally developed as part of libpq core and +later split out as its own shared library in order to isolate its dependency on +libcurl. (End users who don't want the Curl dependency can simply choose not to +install this module.) + +If a connection string allows the use of OAuth, and the server asks for it, and +a libpq client has not installed its own custom OAuth flow, libpq will attempt +to delay-load this module using dlopen() and the following ABI. Failure to load +results in a failed connection. + += Load-Time ABI = + +This module ABI is an internal implementation detail, so it's subject to change +across releases; the name of the module (libpq-oauth-MAJOR-MINOR) reflects this. +The module exports the following symbols: + +- PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +- void pg_fe_cleanup_oauth_flow(PGconn *conn); + +pg_fe_run_oauth_flow and pg_fe_cleanup_oauth_flow are implementations of +conn->async_auth and conn->cleanup_async_auth, respectively. + +- void libpq_oauth_init(pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + +At the moment, pg_fe_run_oauth_flow() relies on libpq's pg_g_threadlock and +libpq_gettext(), which must be injected by libpq using this initialization +function before the flow is run. + +It also relies on libpq to expose conn->errorMessage, via the errmsg_impl. This +is done to decouple the module ABI from the offset of errorMessage, which can +change positions depending on configure-time options. This way we can safely +search the standard dlopen() paths (e.g. RPATH, LD_LIBRARY_PATH, the SO cache) +for an implementation module to use, even if that module wasn't compiled at the +same time as libpq. + += Static Build = + +The static library libpq.a does not perform any dynamic loading. If the builtin +flow is enabled, the application is expected to link against libpq-oauth.a +directly to provide the necessary symbols. diff --git a/src/interfaces/libpq-oauth/exports.txt b/src/interfaces/libpq-oauth/exports.txt new file mode 100644 index 00000000000..6891a83dbf9 --- /dev/null +++ b/src/interfaces/libpq-oauth/exports.txt @@ -0,0 +1,4 @@ +# src/interfaces/libpq-oauth/exports.txt +libpq_oauth_init 1 +pg_fe_run_oauth_flow 2 +pg_fe_cleanup_oauth_flow 3 diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build new file mode 100644 index 00000000000..d97f893178a --- /dev/null +++ b/src/interfaces/libpq-oauth/meson.build @@ -0,0 +1,45 @@ +# Copyright (c) 2022-2025, PostgreSQL Global Development Group + +if not oauth_flow_supported + subdir_done() +endif + +libpq_oauth_sources = files( + 'oauth-curl.c', +) + +# The shared library needs additional glue symbols. +libpq_oauth_so_sources = files( + 'oauth-utils.c', +) +libpq_oauth_so_c_args = ['-DUSE_DYNAMIC_OAUTH'] + +export_file = custom_target('libpq-oauth.exports', + kwargs: gen_export_kwargs, +) + +# port needs to be in include path due to pthread-win32.h +libpq_oauth_inc = include_directories('.', '../libpq', '../../port') + +libpq_oauth_st = static_library('libpq-oauth', + libpq_oauth_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_pch: pch_postgres_fe_h, + dependencies: [frontend_stlib_code, libpq_oauth_deps], + kwargs: default_lib_args, +) + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. +libpq_oauth_name = 'libpq-oauth-@0@-@1@'.format(pg_version_major, pg_version_minor) + +libpq_oauth_so = shared_module(libpq_oauth_name, + libpq_oauth_sources + libpq_oauth_so_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_args: libpq_so_c_args, + c_pch: pch_postgres_fe_h, + dependencies: [frontend_shlib_code, libpq, libpq_oauth_deps], + link_depends: export_file, + link_args: export_fmt.format(export_file.full_path()), + kwargs: default_lib_args, +) diff --git a/src/interfaces/libpq/fe-auth-oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c similarity index 97% rename from src/interfaces/libpq/fe-auth-oauth-curl.c rename to src/interfaces/libpq-oauth/oauth-curl.c index c195e00cd28..7b38395ec5f 100644 --- a/src/interfaces/libpq/fe-auth-oauth-curl.c +++ b/src/interfaces/libpq-oauth/oauth-curl.c @@ -1,6 +1,6 @@ /*------------------------------------------------------------------------- * - * fe-auth-oauth-curl.c + * oauth-curl.c * The libcurl implementation of OAuth/OIDC authentication, using the * OAuth Device Authorization Grant (RFC 8628). * @@ -8,7 +8,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * src/interfaces/libpq/fe-auth-oauth-curl.c + * src/interfaces/libpq-oauth/oauth-curl.c * *------------------------------------------------------------------------- */ @@ -17,20 +17,25 @@ #include <curl/curl.h> #include <math.h> -#ifdef HAVE_SYS_EPOLL_H +#include <unistd.h> + +#if defined(HAVE_SYS_EPOLL_H) #include <sys/epoll.h> #include <sys/timerfd.h> -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) #include <sys/event.h> +#else +#error libpq-oauth is not supported on this platform #endif -#include <unistd.h> #include "common/jsonapi.h" #include "fe-auth.h" #include "fe-auth-oauth.h" -#include "libpq-int.h" #include "mb/pg_wchar.h" +#include "oauth-curl.h" +#ifdef USE_DYNAMIC_OAUTH +#include "oauth-utils.h" +#endif /* * It's generally prudent to set a maximum response size to buffer in memory, @@ -1110,7 +1115,7 @@ parse_access_token(struct async_ctx *actx, struct token *tok) static bool setup_multiplexer(struct async_ctx *actx) { -#ifdef HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct epoll_event ev = {.events = EPOLLIN}; actx->mux = epoll_create1(EPOLL_CLOEXEC); @@ -1134,8 +1139,7 @@ setup_multiplexer(struct async_ctx *actx) } return true; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) actx->mux = kqueue(); if (actx->mux < 0) { @@ -1158,10 +1162,9 @@ setup_multiplexer(struct async_ctx *actx) } return true; +#else +#error setup_multiplexer is not implemented on this platform #endif - - actx_error(actx, "libpq does not support the Device Authorization flow on this platform"); - return false; } /* @@ -1174,7 +1177,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, { struct async_ctx *actx = ctx; -#ifdef HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct epoll_event ev = {0}; int res; int op = EPOLL_CTL_ADD; @@ -1230,8 +1233,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, } return 0; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) struct kevent ev[2] = {0}; struct kevent ev_out[2]; struct timespec timeout = {0}; @@ -1312,10 +1314,9 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, } return 0; +#else +#error register_socket is not implemented on this platform #endif - - actx_error(actx, "libpq does not support multiplexer sockets on this platform"); - return -1; } /* @@ -1334,7 +1335,7 @@ register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, static bool set_timer(struct async_ctx *actx, long timeout) { -#if HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct itimerspec spec = {0}; if (timeout < 0) @@ -1363,8 +1364,7 @@ set_timer(struct async_ctx *actx, long timeout) } return true; -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) struct kevent ev; #ifdef __NetBSD__ @@ -1419,10 +1419,9 @@ set_timer(struct async_ctx *actx, long timeout) } return true; +#else +#error set_timer is not implemented on this platform #endif - - actx_error(actx, "libpq does not support timers on this platform"); - return false; } /* @@ -1433,7 +1432,7 @@ set_timer(struct async_ctx *actx, long timeout) static int timer_expired(struct async_ctx *actx) { -#if HAVE_SYS_EPOLL_H +#if defined(HAVE_SYS_EPOLL_H) struct itimerspec spec = {0}; if (timerfd_gettime(actx->timerfd, &spec) < 0) @@ -1453,8 +1452,7 @@ timer_expired(struct async_ctx *actx) /* If the remaining time to expiration is zero, we're done. */ return (spec.it_value.tv_sec == 0 && spec.it_value.tv_nsec == 0); -#endif -#ifdef HAVE_SYS_EVENT_H +#elif defined(HAVE_SYS_EVENT_H) int res; /* Is the timer queue ready? */ @@ -1466,10 +1464,9 @@ timer_expired(struct async_ctx *actx) } return (res > 0); +#else +#error timer_expired is not implemented on this platform #endif - - actx_error(actx, "libpq does not support timers on this platform"); - return -1; } /* @@ -2487,8 +2484,9 @@ prompt_user(struct async_ctx *actx, PGconn *conn) .verification_uri_complete = actx->authz.verification_uri_complete, .expires_in = actx->authz.expires_in, }; + PQauthDataHook_type hook = PQgetAuthDataHook(); - res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); + res = hook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); if (!res) { @@ -2635,6 +2633,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) { fe_oauth_state *state = conn->sasl_state; struct async_ctx *actx; + PQExpBuffer errbuf; if (!initialize_curl(conn)) return PGRES_POLLING_FAILED; @@ -2825,41 +2824,43 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) error_return: + /* + * For the dynamic module build, we can't safely rely on the offset of + * conn->errorMessage, since it depends on build options like USE_SSL et + * al. libpq gives us a translator function instead. + */ +#ifdef USE_DYNAMIC_OAUTH + errbuf = conn_errorMessage(conn); +#else + errbuf = &conn->errorMessage; +#endif + /* * Assemble the three parts of our error: context, body, and detail. See * also the documentation for struct async_ctx. */ if (actx->errctx) - { - appendPQExpBufferStr(&conn->errorMessage, - libpq_gettext(actx->errctx)); - appendPQExpBufferStr(&conn->errorMessage, ": "); - } + appendPQExpBuffer(errbuf, "%s: ", libpq_gettext(actx->errctx)); if (PQExpBufferDataBroken(actx->errbuf)) - appendPQExpBufferStr(&conn->errorMessage, - libpq_gettext("out of memory")); + appendPQExpBufferStr(errbuf, libpq_gettext("out of memory")); else - appendPQExpBufferStr(&conn->errorMessage, actx->errbuf.data); + appendPQExpBufferStr(errbuf, actx->errbuf.data); if (actx->curl_err[0]) { - size_t len; - - appendPQExpBuffer(&conn->errorMessage, - " (libcurl: %s)", actx->curl_err); + appendPQExpBuffer(errbuf, " (libcurl: %s)", actx->curl_err); /* Sometimes libcurl adds a newline to the error buffer. :( */ - len = conn->errorMessage.len; - if (len >= 2 && conn->errorMessage.data[len - 2] == '\n') + if (errbuf->len >= 2 && errbuf->data[errbuf->len - 2] == '\n') { - conn->errorMessage.data[len - 2] = ')'; - conn->errorMessage.data[len - 1] = '\0'; - conn->errorMessage.len--; + errbuf->data[errbuf->len - 2] = ')'; + errbuf->data[errbuf->len - 1] = '\0'; + errbuf->len--; } } - appendPQExpBufferChar(&conn->errorMessage, '\n'); + appendPQExpBufferChar(errbuf, '\n'); return PGRES_POLLING_FAILED; } diff --git a/src/interfaces/libpq-oauth/oauth-curl.h b/src/interfaces/libpq-oauth/oauth-curl.h new file mode 100644 index 00000000000..248d0424ad0 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-curl.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * oauth-curl.h + * + * Definitions for OAuth Device Authorization module + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-curl.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_CURL_H +#define OAUTH_CURL_H + +#include "libpq-fe.h" + +/* Exported async-auth callbacks. */ +extern PGDLLEXPORT PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +extern PGDLLEXPORT void pg_fe_cleanup_oauth_flow(PGconn *conn); + +#endif /* OAUTH_CURL_H */ diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c new file mode 100644 index 00000000000..1f85a6b0479 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.c @@ -0,0 +1,202 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.c + * + * "Glue" helpers providing a copy of some internal APIs from libpq. At + * some point in the future, we might be able to deduplicate. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq-oauth/oauth-utils.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include <signal.h> + +#include "libpq-int.h" +#include "oauth-utils.h" + +#ifndef USE_DYNAMIC_OAUTH +#error oauth-utils.c is not supported in static builds +#endif + +static libpq_gettext_func libpq_gettext_impl; + +pgthreadlock_t pg_g_threadlock; +conn_errorMessage_func conn_errorMessage; + +/*- + * Initializes libpq-oauth by setting necessary callbacks. + * + * The current implementation relies on the following private implementation + * details of libpq: + * + * - pg_g_threadlock: protects libcurl initialization if the underlying Curl + * installation is not threadsafe + * + * - libpq_gettext: translates error messages using libpq's message domain + * + * - conn->errorMessage: holds translated errors for the connection. This is + * handled through a translation shim, which avoids either depending on the + * offset of the errorMessage in PGconn, or needing to export the variadic + * libpq_append_conn_error(). + */ +void +libpq_oauth_init(pgthreadlock_t threadlock_impl, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl) +{ + pg_g_threadlock = threadlock_impl; + libpq_gettext_impl = gettext_impl; + conn_errorMessage = errmsg_impl; +} + +/* + * Append a formatted string to the error message buffer of the given + * connection, after translating it. This is a copy of libpq's internal API. + */ +void +libpq_append_conn_error(PGconn *conn, const char *fmt,...) +{ + int save_errno = errno; + bool done; + va_list args; + PQExpBuffer errorMessage = conn_errorMessage(conn); + + Assert(fmt[strlen(fmt) - 1] != '\n'); + + if (PQExpBufferBroken(errorMessage)) + return; /* already failed */ + + /* Loop in case we have to retry after enlarging the buffer. */ + do + { + errno = save_errno; + va_start(args, fmt); + done = appendPQExpBufferVA(errorMessage, libpq_gettext(fmt), args); + va_end(args); + } while (!done); + + appendPQExpBufferChar(errorMessage, '\n'); +} + +#ifdef ENABLE_NLS + +/* + * A shim that defers to the actual libpq_gettext(). + */ +char * +libpq_gettext(const char *msgid) +{ + if (!libpq_gettext_impl) + { + /* + * Possible if the libpq build didn't enable NLS but the libpq-oauth + * build did. That's an odd mismatch, but we can handle it. + * + * Note that callers of libpq_gettext() have to treat the return value + * as if it were const, because builds without NLS simply pass through + * their argument. + */ + return unconstify(char *, msgid); + } + + return libpq_gettext_impl(msgid); +} + +#endif /* ENABLE_NLS */ + +/* + * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment. + */ +bool +oauth_unsafe_debugging_enabled(void) +{ + const char *env = getenv("PGOAUTHDEBUG"); + + return (env && strcmp(env, "UNSAFE") == 0); +} + +/* + * Duplicate SOCK_ERRNO* definitions from libpq-int.h, for use by + * pq_block/reset_sigpipe(). + */ +#ifdef WIN32 +#define SOCK_ERRNO (WSAGetLastError()) +#define SOCK_ERRNO_SET(e) WSASetLastError(e) +#else +#define SOCK_ERRNO errno +#define SOCK_ERRNO_SET(e) (errno = (e)) +#endif + +/* + * Block SIGPIPE for this thread. This is a copy of libpq's internal API. + */ +int +pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending) +{ + sigset_t sigpipe_sigset; + sigset_t sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + /* Block SIGPIPE and save previous mask for later reset */ + SOCK_ERRNO_SET(pthread_sigmask(SIG_BLOCK, &sigpipe_sigset, osigset)); + if (SOCK_ERRNO) + return -1; + + /* We can have a pending SIGPIPE only if it was blocked before */ + if (sigismember(osigset, SIGPIPE)) + { + /* Is there a pending SIGPIPE? */ + if (sigpending(&sigset) != 0) + return -1; + + if (sigismember(&sigset, SIGPIPE)) + *sigpipe_pending = true; + else + *sigpipe_pending = false; + } + else + *sigpipe_pending = false; + + return 0; +} + +/* + * Discard any pending SIGPIPE and reset the signal mask. This is a copy of + * libpq's internal API. + */ +void +pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe) +{ + int save_errno = SOCK_ERRNO; + int signo; + sigset_t sigset; + + /* Clear SIGPIPE only if none was pending */ + if (got_epipe && !sigpipe_pending) + { + if (sigpending(&sigset) == 0 && + sigismember(&sigset, SIGPIPE)) + { + sigset_t sigpipe_sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + sigwait(&sigpipe_sigset, &signo); + } + } + + /* Restore saved block mask */ + pthread_sigmask(SIG_SETMASK, osigset, NULL); + + SOCK_ERRNO_SET(save_errno); +} diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h new file mode 100644 index 00000000000..e2a9d01237d --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.h @@ -0,0 +1,38 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.h + * + * Definitions providing missing libpq internal APIs + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-utils.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_UTILS_H +#define OAUTH_UTILS_H + +#include "libpq-fe.h" +#include "pqexpbuffer.h" + +typedef char *(*libpq_gettext_func) (const char *msgid); +typedef PQExpBuffer (*conn_errorMessage_func) (PGconn *conn); + +/* Initializes libpq-oauth. */ +extern PGDLLEXPORT void libpq_oauth_init(pgthreadlock_t threadlock, + libpq_gettext_func gettext_impl, + conn_errorMessage_func errmsg_impl); + +/* Callback to safely obtain conn->errorMessage from a PGconn. */ +extern conn_errorMessage_func conn_errorMessage; + +/* Duplicated APIs, copied from libpq. */ +extern void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3); +extern bool oauth_unsafe_debugging_enabled(void); +extern int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending); +extern void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe); + +#endif /* OAUTH_UTILS_H */ diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 90b0b65db6f..c6fe5fec7f6 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -31,7 +31,6 @@ endif OBJS = \ $(WIN32RES) \ - fe-auth-oauth.o \ fe-auth-scram.o \ fe-cancel.o \ fe-connect.o \ @@ -64,9 +63,11 @@ OBJS += \ fe-secure-gssapi.o endif -ifeq ($(with_libcurl),yes) -OBJS += fe-auth-oauth-curl.o -endif +# The OAuth implementation differs depending on the type of library being built. +OBJS_STATIC = fe-auth-oauth.o + +fe-auth-oauth_shlib.o: override CPPFLAGS_SHLIB += -DUSE_DYNAMIC_OAUTH +OBJS_SHLIB = fe-auth-oauth_shlib.o ifeq ($(PORTNAME), cygwin) override shlib = cyg$(NAME)$(DLSUFFIX) @@ -86,7 +87,7 @@ endif # that are built correctly for use in a shlib. SHLIB_LINK_INTERNAL = -lpgcommon_shlib -lpgport_shlib ifneq ($(PORTNAME), win32) -SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lcurl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) +SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) else SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi32 -lssl -lsocket -lnsl -lresolv -lintl -lm $(PTHREAD_LIBS), $(LIBS)) $(LDAP_LIBS_FE) endif @@ -101,12 +102,26 @@ ifeq ($(with_ssl),openssl) PKG_CONFIG_REQUIRES_PRIVATE = libssl, libcrypto endif +ifeq ($(with_libcurl),yes) +# libpq.so doesn't link against libcurl, but libpq.a needs libpq-oauth, and +# libpq-oauth needs libcurl. Put both into *.private. +PKG_CONFIG_REQUIRES_PRIVATE += libcurl +%.pc: override SHLIB_LINK_INTERNAL += -lpq-oauth +endif + all: all-lib libpq-refs-stamp # Shared library stuff include $(top_srcdir)/src/Makefile.shlib backend_src = $(top_srcdir)/src/backend +# Add shlib-/stlib-specific objects. +$(shlib): override OBJS += $(OBJS_SHLIB) +$(shlib): $(OBJS_SHLIB) + +$(stlib): override OBJS += $(OBJS_STATIC) +$(stlib): $(OBJS_STATIC) + # Check for functions that libpq must not call, currently just exit(). # (Ideally we'd reject abort() too, but there are various scenarios where # build toolchains insert abort() calls, e.g. to implement assert().) @@ -115,8 +130,6 @@ backend_src = $(top_srcdir)/src/backend # which seems to insert references to that even in pure C code. Excluding # __tsan_func_exit is necessary when using ThreadSanitizer data race detector # which use this function for instrumentation of function exit. -# libcurl registers an exit handler in the memory debugging code when running -# with LeakSanitizer. # Skip the test when profiling, as gcc may insert exit() calls for that. # Also skip the test on platforms where libpq infrastructure may be provided # by statically-linked libraries, as we can't expect them to honor this @@ -124,7 +137,7 @@ backend_src = $(top_srcdir)/src/backend libpq-refs-stamp: $(shlib) ifneq ($(enable_coverage), yes) ifeq (,$(filter solaris,$(PORTNAME))) - @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit -e _atexit | grep exit; then \ + @if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit | grep exit; then \ echo 'libpq must not be calling any function which invokes exit'; exit 1; \ fi endif @@ -138,6 +151,11 @@ fe-misc.o: fe-misc.c $(top_builddir)/src/port/pg_config_paths.h $(top_builddir)/src/port/pg_config_paths.h: $(MAKE) -C $(top_builddir)/src/port pg_config_paths.h +# Use src/common/Makefile's trick for tracking dependencies of shlib-specific +# objects. +%_shlib.o: %.c %.o + $(CC) $(CFLAGS) $(CFLAGS_SL) $(CPPFLAGS) $(CPPFLAGS_SHLIB) -c $< -o $@ + install: all installdirs install-lib $(INSTALL_DATA) $(srcdir)/libpq-fe.h '$(DESTDIR)$(includedir)' $(INSTALL_DATA) $(srcdir)/libpq-events.h '$(DESTDIR)$(includedir)' @@ -171,6 +189,6 @@ uninstall: uninstall-lib clean distclean: clean-lib $(MAKE) -C test $@ rm -rf tmp_check - rm -f $(OBJS) pthread.h libpq-refs-stamp + rm -f $(OBJS) $(OBJS_SHLIB) $(OBJS_STATIC) pthread.h libpq-refs-stamp # Might be left over from a Win32 client-only build rm -f pg_config_paths.h diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index d5143766858..0625cf39e9a 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -210,3 +210,4 @@ PQsetAuthDataHook 207 PQgetAuthDataHook 208 PQdefaultAuthDataHook 209 PQfullProtocolVersion 210 +appendPQExpBufferVA 211 diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c index cf1a25e2ccc..ccdd9139cf1 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,186 @@ 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); +typedef PQExpBuffer (*conn_errorMessage_func) (PGconn *conn); + +/* + * This shim is injected into libpq-oauth so that it doesn't depend on the + * offset of conn->errorMessage. + * + * TODO: look into exporting libpq_append_conn_error or a comparable API from + * libpq, instead. + */ +static PQExpBuffer +conn_errorMessage(PGconn *conn) +{ + return &conn->errorMessage; +} + +/* + * 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); + 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 "-" PG_MINORVERSION DLSUFFIX; +#else + "libpq-oauth-" PG_MAJORVERSION "-" PG_MINORVERSION 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); + + 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 +977,10 @@ setup_token_request(PGconn *conn, fe_oauth_state *state) libpq_append_conn_error(conn, "user-defined OAuth flow failed"); goto fail; } - else + else if (!use_builtin_flow(conn, state)) { -#if USE_LIBCURL - /* Hand off to our built-in OAuth flow. */ - conn->async_auth = pg_fe_run_oauth_flow; - conn->cleanup_async_auth = pg_fe_cleanup_oauth_flow; - -#else - libpq_append_conn_error(conn, "no custom OAuth flows are available, and libpq was not built with libcurl support"); + libpq_append_conn_error(conn, "no OAuth flows are available (try installing the libpq-oauth package)"); goto fail; - -#endif } return true; diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h index 3f1a7503a01..687e664475f 100644 --- a/src/interfaces/libpq/fe-auth-oauth.h +++ b/src/interfaces/libpq/fe-auth-oauth.h @@ -33,12 +33,13 @@ typedef struct PGconn *conn; void *async_ctx; + + void *builtin_flow; } fe_oauth_state; -extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); -extern void pg_fe_cleanup_oauth_flow(PGconn *conn); extern void pqClearOAuthToken(PGconn *conn); extern bool oauth_unsafe_debugging_enabled(void); +extern bool use_builtin_flow(PGconn *conn, fe_oauth_state *state); /* Mechanisms in fe-auth-oauth.c */ extern const pg_fe_sasl_mech pg_oauth_mech; diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build index 292fecf3320..a74e885b169 100644 --- a/src/interfaces/libpq/meson.build +++ b/src/interfaces/libpq/meson.build @@ -38,10 +38,6 @@ if gssapi.found() ) endif -if libcurl.found() - libpq_sources += files('fe-auth-oauth-curl.c') -endif - export_file = custom_target('libpq.exports', kwargs: gen_export_kwargs, ) @@ -50,6 +46,9 @@ export_file = custom_target('libpq.exports', libpq_inc = include_directories('.', '../../port') libpq_c_args = ['-DSO_MAJOR_VERSION=5'] +# The OAuth implementation differs depending on the type of library being built. +libpq_so_c_args = ['-DUSE_DYNAMIC_OAUTH'] + # Not using both_libraries() here as # 1) resource files should only be in the shared library # 2) we want the .pc file to include a dependency to {pgport,common}_static for @@ -70,7 +69,7 @@ libpq_st = static_library('libpq', libpq_so = shared_library('libpq', libpq_sources + libpq_so_sources, include_directories: [libpq_inc, postgres_inc], - c_args: libpq_c_args, + c_args: libpq_c_args + libpq_so_c_args, c_pch: pch_postgres_fe_h, version: '5.' + pg_version_major.to_string(), soversion: host_system != 'windows' ? '5' : '', @@ -86,12 +85,26 @@ libpq = declare_dependency( include_directories: [include_directories('.')] ) +private_deps = [ + frontend_stlib_code, + libpq_deps, +] + +if oauth_flow_supported + # libpq.so doesn't link against libcurl, but libpq.a needs libpq-oauth, and + # libpq-oauth needs libcurl. Put both into *.private. + private_deps += [ + libpq_oauth_deps, + '-lpq-oauth', + ] +endif + pkgconfig.generate( name: 'libpq', description: 'PostgreSQL libpq library', url: pg_url, libraries: libpq, - libraries_private: [frontend_stlib_code, libpq_deps], + libraries_private: private_deps, ) install_headers( diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk index ae761265852..b87df277d93 100644 --- a/src/interfaces/libpq/nls.mk +++ b/src/interfaces/libpq/nls.mk @@ -13,15 +13,21 @@ GETTEXT_FILES = fe-auth.c \ fe-secure-common.c \ fe-secure-gssapi.c \ fe-secure-openssl.c \ - win32.c -GETTEXT_TRIGGERS = libpq_append_conn_error:2 \ + win32.c \ + ../libpq-oauth/oauth-curl.c \ + ../libpq-oauth/oauth-utils.c +GETTEXT_TRIGGERS = actx_error:2 \ + libpq_append_conn_error:2 \ libpq_append_error:2 \ libpq_gettext \ libpq_ngettext:1,2 \ + oauth_parse_set_error:2 \ pqInternalNotice:2 -GETTEXT_FLAGS = libpq_append_conn_error:2:c-format \ +GETTEXT_FLAGS = actx_error:2:c-format \ + libpq_append_conn_error:2:c-format \ libpq_append_error:2:c-format \ libpq_gettext:1:pass-c-format \ libpq_ngettext:1:pass-c-format \ libpq_ngettext:2:pass-c-format \ + oauth_parse_set_error:2:c-format \ pqInternalNotice:2:c-format diff --git a/src/makefiles/meson.build b/src/makefiles/meson.build index e3adb5d8dc4..48d01a54dc6 100644 --- a/src/makefiles/meson.build +++ b/src/makefiles/meson.build @@ -204,6 +204,8 @@ pgxs_empty = [ 'LIBNUMA_CFLAGS', 'LIBNUMA_LIBS', 'LIBURING_CFLAGS', 'LIBURING_LIBS', + + 'LIBCURL_CPPFLAGS', 'LIBCURL_LDFLAGS', 'LIBCURL_LDLIBS', ] if host_system == 'windows' and cc.get_argument_syntax() != 'msvc' diff --git a/src/test/modules/oauth_validator/meson.build b/src/test/modules/oauth_validator/meson.build index 36d1b26369f..e190f9cf15a 100644 --- a/src/test/modules/oauth_validator/meson.build +++ b/src/test/modules/oauth_validator/meson.build @@ -78,7 +78,7 @@ tests += { ], 'env': { 'PYTHON': python.path(), - 'with_libcurl': libcurl.found() ? 'yes' : 'no', + 'with_libcurl': oauth_flow_supported ? 'yes' : 'no', 'with_python': 'yes', }, }, diff --git a/src/test/modules/oauth_validator/t/002_client.pl b/src/test/modules/oauth_validator/t/002_client.pl index 8dd502f41e1..21d4acc1926 100644 --- a/src/test/modules/oauth_validator/t/002_client.pl +++ b/src/test/modules/oauth_validator/t/002_client.pl @@ -110,7 +110,7 @@ if ($ENV{with_libcurl} ne 'yes') "fails without custom hook installed", flags => ["--no-hook"], expected_stderr => - qr/no custom OAuth flows are available, and libpq was not built with libcurl support/ + qr/no OAuth flows are available \(try installing the libpq-oauth package\)/ ); } -- 2.34.1 ^ permalink raw reply [nested|flat] 36+ messages in thread
end of thread, other threads:[~2025-04-22 23:41 UTC | newest] Thread overview: 36+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2021-02-10 23:19 [PATCH 18/18] doc review for pg_stat_progress_create_index Justin Pryzby <[email protected]> 2025-04-07 13:59 Re: [PoC] Federated Authn/z with OAUTHBEARER Peter Eisentraut <[email protected]> 2025-04-07 14:43 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Andres Freund <[email protected]> 2025-04-07 16:38 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Peter Eisentraut <[email protected]> 2025-04-07 16:52 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Andres Freund <[email protected]> 2025-04-07 16:41 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-07 17:05 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Andres Freund <[email protected]> 2025-04-07 17:32 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-07 18:47 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Christoph Berg <[email protected]> 2025-04-07 21:49 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-07 21:58 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Christoph Berg <[email protected]> 2025-04-07 22:26 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-08 02:10 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-08 21:32 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-09 02:57 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-09 08:14 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Christoph Berg <[email protected]> 2025-04-09 12:50 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-09 17:44 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Christoph Berg <[email protected]> 2025-04-09 23:08 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-09 23:42 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jelte Fennema-Nio <[email protected]> 2025-04-11 00:12 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-15 12:31 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Peter Eisentraut <[email protected]> 2025-04-15 15:34 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Christoph Berg <[email protected]> 2025-04-15 18:02 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-15 17:53 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-15 18:57 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Christoph Berg <[email protected]> 2025-04-15 19:44 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-15 21:38 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jelte Fennema-Nio <[email protected]> 2025-04-18 00:47 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-18 17:01 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-19 12:03 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Christoph Berg <[email protected]> 2025-04-21 23:19 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-22 10:02 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Daniel Gustafsson <[email protected]> 2025-04-22 23:41 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]> 2025-04-15 19:21 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Robert Haas <[email protected]> 2025-04-15 19:45 ` Re: [PoC] Federated Authn/z with OAUTHBEARER Jacob Champion <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox