From 1decdddc5bd95b266e582923d5961ad2ecfbed98 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Thu, 27 Aug 2026 02:12:10 +0000 Subject: [PATCH] Add PAM tests This commit adds tests for the PAM functionality in postgres. In order to implement tests while not using sudo or assuming a particular configuration was set up ahead of time, this commit also adds the ability to use custom config locations via the pam_start_confdir function. This pam_start_confdir is documented in the man page for pam_start [0], it has the same interface as pam_start but adds an additional confdir parameter. When the confdir parameter is set to NULL the behaviour is the same as pam_start. We expose the confdir parameter via a new pg_hba.conf pam option pamconfdir. While this was mostly implemented for the sake of testing this has some utility outside of testing such as environments where DBA's are not allowed root access. pam_start_confdir was added to the 1.4.0 release of pam [1] and as such if postgres is built on an older release the functionality is not available. We use ifdef gates and HAVE_PAM_START_CONFDIR to remove this functionality from older builds. [0] https://man7.org/linux/man-pages/man3/pam_start.3.html [1] https://github.com/linux-pam/linux-pam/blob/dd74fc113a9ba1f94d5469f6f7857a1884b3f550/NEWS#L192 --- configure | 13 ++++ configure.ac | 2 + doc/src/sgml/client-auth.sgml | 14 ++++ meson.build | 3 + src/Makefile.global.in | 1 + src/backend/libpq/auth.c | 21 +++++- src/backend/libpq/hba.c | 16 +++++ src/include/libpq/hba.h | 4 +- src/include/pg_config.h.in | 3 + src/test/Makefile | 3 + src/test/README | 3 + src/test/meson.build | 4 ++ src/test/pam/.gitignore | 2 + src/test/pam/Makefile | 26 ++++++++ src/test/pam/README | 48 ++++++++++++++ src/test/pam/meson.build | 16 +++++ src/test/pam/t/001_auth.pl | 121 ++++++++++++++++++++++++++++++++++ 17 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 src/test/pam/.gitignore create mode 100644 src/test/pam/Makefile create mode 100644 src/test/pam/README create mode 100644 src/test/pam/meson.build create mode 100644 src/test/pam/t/001_auth.pl diff --git a/configure b/configure index d42a7a794ff..05fdba9a0de 100755 --- a/configure +++ b/configure @@ -726,6 +726,7 @@ with_readline with_systemd with_selinux with_ldap +with_pam with_krb_srvnam krb_srvtab with_gssapi @@ -8461,6 +8462,7 @@ fi $as_echo "$with_pam" >&6; } + # # BSD AUTH # @@ -13245,6 +13247,17 @@ else as_fn_error $? "library 'pam' is required for PAM" "$LINENO" 5 fi + for ac_func in pam_start_confdir +do : + ac_fn_c_check_func "$LINENO" "pam_start_confdir" "ac_cv_func_pam_start_confdir" +if test "x$ac_cv_func_pam_start_confdir" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_PAM_START_CONFDIR 1 +_ACEOF + +fi +done + fi if test "$with_libxml" = yes ; then diff --git a/configure.ac b/configure.ac index a331749fcb5..2c2e9634de2 100644 --- a/configure.ac +++ b/configure.ac @@ -942,6 +942,7 @@ PGAC_ARG_BOOL(with, pam, no, [build with PAM support], [AC_DEFINE([USE_PAM], 1, [Define to 1 to build with PAM support. (--with-pam)])]) AC_MSG_RESULT([$with_pam]) +AC_SUBST(with_pam) # @@ -1452,6 +1453,7 @@ AC_SUBST(with_ssl) if test "$with_pam" = yes ; then AC_CHECK_LIB(pam, pam_start, [], [AC_MSG_ERROR([library 'pam' is required for PAM])]) + AC_CHECK_FUNCS(pam_start_confdir) fi if test "$with_libxml" = yes ; then diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml index e4e65f8feb1..2ca5a763ac2 100644 --- a/doc/src/sgml/client-auth.sgml +++ b/doc/src/sgml/client-auth.sgml @@ -2169,6 +2169,20 @@ host ... ldap ldapbasedn="dc=example,dc=net" + + pamconfdir + + + If specified, use the pam configuration directory specified instead + of the usual /etc/pam.d/ directory. This is + useful if the default configuration directory is inaccessible or + undesitable to use. Please note that note that this makes use of the + pam_start_confdir<\symbol> function and as such will not be + usable by any PAM implementation that does not support this function. + (On linux-pam this was introduced in 1.4.0.) + + + diff --git a/meson.build b/meson.build index f4cde249242..0876ac6eec3 100644 --- a/meson.build +++ b/meson.build @@ -1245,10 +1245,12 @@ if not pamopt.disabled() args: test_c_args, include_directories: postgres_inc) cdata.set('HAVE_SECURITY_PAM_APPL_H', 1) pam_header_found = true + pam_header_fp = 'security/pam_appl.h' elif cc.check_header('pam/pam_appl.h', dependencies: pam, required: pamopt, args: test_c_args, include_directories: postgres_inc) cdata.set('HAVE_PAM_PAM_APPL_H', 1) pam_header_found = true + pam_header_fp = 'pam/pam_appl.h' endif if pam_header_found @@ -3228,6 +3230,7 @@ func_checks = [ ['strlcat'], ['strlcpy'], ['strsep'], + ['pam_start_confdir', {'dependencies': [pam]}], ['strsignal'], ['sync_file_range'], ['syncfs'], diff --git a/src/Makefile.global.in b/src/Makefile.global.in index cef1ad7f87d..1091b0c0a43 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -195,6 +195,7 @@ with_systemd = @with_systemd@ with_gssapi = @with_gssapi@ with_krb_srvnam = @with_krb_srvnam@ with_ldap = @with_ldap@ +with_pam = @with_pam@ with_libcurl = @with_libcurl@ with_libnuma = @with_libnuma@ with_liburing = @with_liburing@ diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 12bf153d66f..bfe8fa10433 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -2077,6 +2077,9 @@ CheckPAMAuth(Port *port, const char *user, const char *password) { int retval; pam_handle_t *pamh = NULL; +#ifdef HAVE_PAM_START_CONFDIR + char *pamconfdir = NULL; +#endif /* * We can't entirely rely on PAM to pass through appdata --- it appears @@ -2095,13 +2098,29 @@ CheckPAMAuth(Port *port, const char *user, const char *password) pam_passw_conv.appdata_ptr = unconstify(char *, password); /* from password above, * not allocated */ +#ifdef HAVE_PAM_START_CONFDIR + if (port->hba->pamconfdir && port->hba->pamconfdir[0] != '\0') + pamconfdir = port->hba->pamconfdir; +#endif + /* Optionally, one can set the service name in pg_hba.conf */ + if (port->hba->pamservice && port->hba->pamservice[0] != '\0') +#ifdef HAVE_PAM_START_CONFDIR + retval = pam_start_confdir(port->hba->pamservice, "pgsql@", + &pam_passw_conv, pamconfdir, &pamh); +#else retval = pam_start(port->hba->pamservice, "pgsql@", &pam_passw_conv, &pamh); +#endif else +#ifdef HAVE_PAM_START_CONFDIR + retval = pam_start_confdir(PGSQL_PAM_SERVICE, "pgsql@", + &pam_passw_conv, pamconfdir, &pamh); +#else retval = pam_start(PGSQL_PAM_SERVICE, "pgsql@", - &pam_passw_conv, &pamh); + &pam_passw_conv, &pamh); +#endif if (retval != PAM_SUCCESS) { diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index c0d8a9d8a00..2757dd53ca2 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -43,6 +43,15 @@ #include "utils/memutils.h" #include "utils/varlena.h" +#ifdef USE_PAM +#ifdef HAVE_PAM_PAM_APPL_H +#include +#endif +#ifdef HAVE_SECURITY_PAM_APPL_H +#include +#endif +#endif + #ifdef USE_LDAP #ifdef WIN32 #include @@ -2096,6 +2105,13 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline, REQUIRE_AUTH_OPTION(uaPAM, "pamservice", "pam"); hbaline->pamservice = pstrdup(val); } +#ifdef HAVE_PAM_START_CONFDIR + else if (strcmp(name, "pamconfdir") == 0) + { + REQUIRE_AUTH_OPTION(uaPAM, "pamconfdir", "pam"); + hbaline->pamconfdir = pstrdup(val); + } +#endif else if (strcmp(name, "pam_use_hostname") == 0) { REQUIRE_AUTH_OPTION(uaPAM, "pam_use_hostname", "pam"); diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h index 4aa6258a345..87f9fd1aac1 100644 --- a/src/include/libpq/hba.h +++ b/src/include/libpq/hba.h @@ -15,7 +15,6 @@ #include "nodes/pg_list.h" #include "regex/regex.h" - /* * The following enum represents the authentication methods that * are supported by PostgreSQL. @@ -108,6 +107,9 @@ typedef struct HbaLine UserAuth auth_method; char *usermap; char *pamservice; +#ifdef HAVE_PAM_START_CONFDIR + char *pamconfdir; +#endif bool pam_use_hostname; bool ldaptls; char *ldapscheme; diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in index 661c4a9b168..a7ff5905460 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -300,6 +300,9 @@ /* Define to 1 if you have the header file. */ #undef HAVE_PAM_PAM_APPL_H +/* Define to 1 if you have the `pam_start_confdir' function. */ +#undef HAVE_PAM_START_CONFDIR + /* Define to 1 if you have the `posix_fadvise' function. */ #undef HAVE_POSIX_FADVISE diff --git a/src/test/Makefile b/src/test/Makefile index 3eb0a06abb4..d09cccab9a6 100644 --- a/src/test/Makefile +++ b/src/test/Makefile @@ -31,6 +31,9 @@ endif ifeq ($(with_ldap),yes) SUBDIRS += ldap endif +ifeq ($(with_pam),yes) +SUBDIRS += pam +endif ifeq ($(with_ssl),openssl) SUBDIRS += ssl endif diff --git a/src/test/README b/src/test/README index afdc7676519..545921499e4 100644 --- a/src/test/README +++ b/src/test/README @@ -34,6 +34,9 @@ modules/ Extensions used only or mainly for test purposes, generally not suitable for installing in production databases +pam/ + Tests for PAM-based authentication + perl/ Infrastructure for Perl-based TAP tests diff --git a/src/test/meson.build b/src/test/meson.build index cd45cbf57fb..4e64afad3bb 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -25,4 +25,8 @@ if icu.found() subdir('icu') endif +if pam.found() + subdir('pam') +endif + subdir('perl') diff --git a/src/test/pam/.gitignore b/src/test/pam/.gitignore new file mode 100644 index 00000000000..871e943d50e --- /dev/null +++ b/src/test/pam/.gitignore @@ -0,0 +1,2 @@ +# Generated by test suite +/tmp_check/ diff --git a/src/test/pam/Makefile b/src/test/pam/Makefile new file mode 100644 index 00000000000..817172f0d00 --- /dev/null +++ b/src/test/pam/Makefile @@ -0,0 +1,26 @@ +#------------------------------------------------------------------------- +# +# Makefile for src/test/pam +# +# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group +# Portions Copyright (c) 1994, Regents of the University of California +# +# src/test/pam/Makefile +# +#------------------------------------------------------------------------- + +subdir = src/test/pam +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +export with_pam + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) + +clean distclean: + rm -rf tmp_check + diff --git a/src/test/pam/README b/src/test/pam/README new file mode 100644 index 00000000000..0789c71cd17 --- /dev/null +++ b/src/test/pam/README @@ -0,0 +1,48 @@ +src/test/pam/README + +Tests for PAM functionality +============================ + +This directory contains a test suite for PAM functionality. This +requires a PAM installation. This test suite only tests PAM with a +custom confdir argument in pam_start_confdir so no PAM config is needed +outside of the files created by this test suite. Because of the dependency +on pam_start_confdir, the PAM version must be greatr than or equal to 1.4.0. + +Running the tests +================= + +NOTE: You must have given the --enable-tap-tests argument to configure. + +Run + make check PG_TEST_EXTRA=pam +or + make installcheck PG_TEST_EXTRA=pam + +You can use "make installcheck" if you previously did "make install". +In that case, the code in the installation tree is tested. With +"make check", a temporary installation tree is built from the current +sources and then tested. + +Either way, this test initializes, starts, and stops a test Postgres +cluster, as well setting up temporary non-root pam config files. + +See src/test/perl/README for more info about running these tests. + +Requirements +============ +PAM is required, specifically version 1.4.0 or above. + +Debian/Ubuntu packages: libpam-runtime +(You will already have needed libpam-runtime and libpam0g-dev to build.) + +RHEL/CentOS/Fedora package: pam +(You will already have needed pam and pam-devel to build.) + +FreeBSD: no support for tests +(PAM for FreeBSD does not support the function pam_start_confdir which is +required to run these tests) + +macOS: no support for tests +(PAM for macOS does not support the function pam_start_confdir which is +required to run these tests) diff --git a/src/test/pam/meson.build b/src/test/pam/meson.build new file mode 100644 index 00000000000..64489f3c01f --- /dev/null +++ b/src/test/pam/meson.build @@ -0,0 +1,16 @@ +# Copyright (c) 2022-2026, PostgreSQL Global Development Group + +tests += { + 'name': 'pam', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'tests': [ + 't/001_auth.pl', + ], + 'env': { + 'with_pam': pam.found() ? 'yes' : 'no', + }, + }, +} + diff --git a/src/test/pam/t/001_auth.pl b/src/test/pam/t/001_auth.pl new file mode 100644 index 00000000000..7e09eb62703 --- /dev/null +++ b/src/test/pam/t/001_auth.pl @@ -0,0 +1,121 @@ + +# Copyright (c) 2021-2026, PostgreSQL Global Development Group + +use strict; +use warnings FATAL => 'all'; + +use FindBin; +use lib "$FindBin::RealBin/.."; + +use File::Copy; +use File::Basename; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::Cluster; +use Test::More; + +if ($ENV{with_pam} ne 'yes') +{ + plan skip_all => 'PAM not supported by this build'; +} +elsif (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bpam\b/) +{ + plan skip_all => + 'Potentially unsafe test PAM not enabled in PG_TEST_EXTRA'; +} +elsif (!check_pg_config("#define HAVE_PAM_START_CONFDIR 1")) +{ + plan skip_all => + 'PAM tests requires Postgres build with pam library that supports pam_start_confdir function'; +} + +note "setting up PostgreSQL instance"; + +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->append_conf('postgresql.conf', "log_connections = all\n"); +# Needed to allow connect_fails to inspect postmaster log: +$node->append_conf('postgresql.conf', "log_min_messages = debug2"); +$node->start; + +$node->safe_psql('postgres', 'CREATE USER test0;'); +$node->safe_psql('postgres', 'CREATE USER test1;'); +$node->safe_psql('postgres', 'CREATE USER test2;'); +$node->safe_psql('postgres', 'CREATE USER test3;'); +$node->safe_psql('postgres', 'CREATE USER test4;'); + +note "running tests"; + +my $test_temp_= PostgreSQL::Test::Utils::tempdir("pam-001_auth"); + +my $test_temp = PostgreSQL::Test::Utils::tempdir("pam-001_auth"); +my $test_pam_conf_pg = "$test_temp/postgresql"; +my $test_pam_conf_pg2 = "$test_temp/postgresql2"; +my $test_pam_conf_pg3 = "$test_temp/postgresql3"; + +my $test_pam_exec_dir = PostgreSQL::Test::Utils::tempdir("001_authpam_exec"); +my $test_pam_exec = "$test_pam_exec_dir/user.sh"; +my $test_pam_exec_log = "$test_pam_exec_dir/log"; + +append_to_file($test_pam_exec, qq{#! /bin/sh + +set -x + +read pam_passwd + +if [ "\$PAM_USER" != "test3" ]; then + exit 1 +fi +if [ "\$pam_passwd" != "password3" ]; then + exit 2 +fi +exit 0 +}); + +append_to_file($test_pam_conf_pg, qq{ +auth required pam_permit.so +account required pam_permit.so +}); + +append_to_file($test_pam_conf_pg2, qq{ +auth required pam_deny.so +account required pam_deny.so +}); + +append_to_file($test_pam_conf_pg3, qq{ +auth required pam_exec.so debug expose_authtok log=$test_pam_exec_log /bin/sh $test_pam_exec +account required pam_permit.so +}); + +unlink($node->data_dir . '/pg_hba.conf'); +$node->append_conf( + 'pg_hba.conf', + qq{ +local all test0 pam pamconfdir=$test_temp +local all test1 pam pamconfdir=$test_temp pamservice=postgresql +local all test2 pam pamconfdir=$test_temp pamservice=postgresql2 +local all test3 pam pamconfdir=$test_temp pamservice=postgresql3 +local all test4 pam pamconfdir=$test_temp pamservice=postgresql3 +}); +$node->restart; + +$node->connect_ok("user=test0", + "correctly routes pam.d/postgresql by default, connection succeds due to pam_accept.so"); + +$node->connect_ok("user=test1", + "correctly routes pam.d/postgresql by explicit, connection succeds due to pam_accept.so"); + +$node->connect_fails("user=test2", + "correctly routes to pam.d/postgresql2, connection fails due to pam_deny.sio"); + +$node->connect_ok("user=test3 password=password3", + "correctly routes pam.d/postgresql3, accepted"); + +$node->connect_fails("user=test3 password=wrongpassword3", + "correctly routes to pam.d/postgresql3, rejected due to wrong password"); + +$node->connect_fails("user=test4", + "correctly routes to pam.d/postgresql3, rejected due to wrong user"); + +$node->teardown_node; + +done_testing(); -- 2.50.1 (Apple Git-155)