This patch, mostly the work of John Naylor, provides a hook whereby a module can modify the ldapbindpasswd before it is handed to the ldap server. This is similar in concept to the ssl_passphrase_callback feature, and allows the user not to have to put the cleartext password in the pg_hba.conf file. A trivial test is added which provides an example of such a module.
cheers andrew -- Andrew Dunstan EDB: https://www.enterprisedb.com
From 65af40c5b3c05dcfcb55675dec066fe779382105 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan <and...@dunslane.net> Date: Mon, 19 Dec 2022 11:19:24 -0500 Subject: [PATCH] Add a password handling hook for ldapbindpasswd This hook allows for interception of the ldapbindpasswd in the pg_hba.conf file before it is passed on to the ldap server. The hook function receives a copy of the password as specified in the config file and hands back a pointer to a password string. the default handler simply hands back its input. A test is supplied which implements a module that makes a trivial (rot13) modifiction of the input. The benefit here is that the clear text password no longer needs to be stored in the config file. This is similar in concept to the ssl_passphrase_callback feature. John Naylor, with small modifications by Andrew Dunstan. --- src/backend/libpq/auth.c | 12 +- src/include/libpq/auth.h | 6 + src/test/modules/Makefile | 11 + src/test/modules/ldap_password_func/Makefile | 25 ++ .../modules/ldap_password_func/authdata.ldif | 34 +++ .../ldap_password_func/ldap_password_func.c | 64 +++++ .../modules/ldap_password_func/meson.build | 35 +++ .../t/001_mutated_bindpasswd.pl | 220 ++++++++++++++++++ src/test/modules/meson.build | 1 + 9 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/ldap_password_func/Makefile create mode 100644 src/test/modules/ldap_password_func/authdata.ldif create mode 100644 src/test/modules/ldap_password_func/ldap_password_func.c create mode 100644 src/test/modules/ldap_password_func/meson.build create mode 100644 src/test/modules/ldap_password_func/t/001_mutated_bindpasswd.pl diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index e2f723d188..1ba2fa92ab 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -144,6 +144,10 @@ static int CheckLDAPAuth(Port *port); #define LDAP_OPT_DIAGNOSTIC_MESSAGE LDAP_OPT_ERROR_STRING #endif +/* Default LDAP password mutator hook, can be overridden by a shared library */ +static char* dummy_ldap_password_mutator(char* input); +auth_password_hook_typ ldap_password_hook = dummy_ldap_password_mutator; + #endif /* USE_LDAP */ /*---------------------------------------------------------------- @@ -2370,6 +2374,12 @@ InitializeLDAPConnection(Port *port, LDAP **ldap) #define LDAPS_PORT 636 #endif +static char* +dummy_ldap_password_mutator(char * input) +{ + return input; +} + /* * Return a newly allocated C string copied from "pattern" with all * occurrences of the placeholder "$username" replaced with "user_name". @@ -2498,7 +2508,7 @@ CheckLDAPAuth(Port *port) */ r = ldap_simple_bind_s(ldap, port->hba->ldapbinddn ? port->hba->ldapbinddn : "", - port->hba->ldapbindpasswd ? port->hba->ldapbindpasswd : ""); + port->hba->ldapbindpasswd ? ldap_password_hook(port->hba->ldapbindpasswd) : ""); if (r != LDAP_SUCCESS) { ereport(LOG, diff --git a/src/include/libpq/auth.h b/src/include/libpq/auth.h index d3c189efe3..c29bd17516 100644 --- a/src/include/libpq/auth.h +++ b/src/include/libpq/auth.h @@ -28,4 +28,10 @@ extern void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata, typedef void (*ClientAuthentication_hook_type) (Port *, int); extern PGDLLIMPORT ClientAuthentication_hook_type ClientAuthentication_hook; +/* hook type for password manglers */ +typedef char* (* auth_password_hook_typ)(char* input); + +/* Default LDAP password mutator hook, can be overridden by a shared library */ +extern PGDLLIMPORT auth_password_hook_typ ldap_password_hook; + #endif /* AUTH_H */ diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index c629cbe383..79e3033ec2 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -42,5 +42,16 @@ else ALWAYS_SUBDIRS += ssl_passphrase_callback endif +# Test runs an LDAP server, so only run if ldap is in PG_TEST_EXTRA +ifeq ($(with_ldap),yes) +ifneq (,$(filter ldap,$(PG_TEST_EXTRA))) +SUBDIRS += ldap_password_func +else +ALWAYS_SUBDIRS += ldap_password_func +endif +else +ALWAYS_SUBDIRS += ldap_password_func +endif + $(recurse) $(recurse_always) diff --git a/src/test/modules/ldap_password_func/Makefile b/src/test/modules/ldap_password_func/Makefile new file mode 100644 index 0000000000..3324e04248 --- /dev/null +++ b/src/test/modules/ldap_password_func/Makefile @@ -0,0 +1,25 @@ +# Copyright (c) 2022, PostgreSQL Global Development Group + +# ldap_password_func Makefile + +export with_ldap + +MODULE_big = ldap_password_func +OBJS = ldap_password_func.o $(WIN32RES) +PGFILEDESC = "set hook to mutate ldapbindpasswd" + +TAP_TESTS = 1 + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/ldap_password_func +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + + + diff --git a/src/test/modules/ldap_password_func/authdata.ldif b/src/test/modules/ldap_password_func/authdata.ldif new file mode 100644 index 0000000000..62499e803d --- /dev/null +++ b/src/test/modules/ldap_password_func/authdata.ldif @@ -0,0 +1,34 @@ +# Copyright (c) 2022, PostgreSQL Global Development Group + +dn: dc=example,dc=net +objectClass: top +objectClass: dcObject +objectClass: organization +dc: example +o: ExampleCo + +dn: uid=test1,dc=example,dc=net +objectClass: inetOrgPerson +objectClass: posixAccount +uid: test1 +sn: Lastname +givenName: Firstname +cn: First Test User +displayName: First Test User +uidNumber: 101 +gidNumber: 100 +homeDirectory: /home/test1 +mail: te...@example.net + +dn: uid=test2,dc=example,dc=net +objectClass: inetOrgPerson +objectClass: posixAccount +uid: test2 +sn: Lastname +givenName: Firstname +cn: Second Test User +displayName: Second Test User +uidNumber: 102 +gidNumber: 100 +homeDirectory: /home/test2 +mail: te...@example.net diff --git a/src/test/modules/ldap_password_func/ldap_password_func.c b/src/test/modules/ldap_password_func/ldap_password_func.c new file mode 100644 index 0000000000..8a8ae40187 --- /dev/null +++ b/src/test/modules/ldap_password_func/ldap_password_func.c @@ -0,0 +1,64 @@ +/*------------------------------------------------------------------------- + * + * Copyright (c) 2022, PostgreSQL Global Development Group + * + * ldap_password_func.c + * + * Loadable PostgreSQL module to mutate the ldapbindpasswd. This + * implementation just hands back the configured password rot13'd. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include <float.h> +#include <stdio.h> + +#include "libpq/libpq.h" +#include "libpq/libpq-be.h" +#include "libpq/auth.h" +#include "utils/guc.h" + +PG_MODULE_MAGIC; + +void _PG_init(void); +void _PG_fini(void); + +/* hook function */ +static char* rot13_passphrase(char *password); + +/* + * Module load callback + */ +void +_PG_init(void) +{ + ldap_password_hook = rot13_passphrase; +} + +void +_PG_fini(void) +{ + /* do nothing yet */ +} + +static char* +rot13_passphrase(char *pw) +{ + size_t size = strlen(pw) + 1; + + char* new_pw = (char*) palloc(size); + strlcpy(new_pw, pw, size); + for (char *p = new_pw; *p; p++) + { + char c = *p; + + if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M')) + *p = c + 13; + else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z')) + *p = c - 13; + } + + return new_pw; +} diff --git a/src/test/modules/ldap_password_func/meson.build b/src/test/modules/ldap_password_func/meson.build new file mode 100644 index 0000000000..8ab6d6e8ac --- /dev/null +++ b/src/test/modules/ldap_password_func/meson.build @@ -0,0 +1,35 @@ +if not ldap.found() + subdir_done() +endif + +# FIXME: prevent install during main install, but not during test :/ + +ldap_password_func_sources = files( + 'ldap_password_func.c', +) + +if host_system == 'windows' + ldap_password_func_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ + '--NAME', 'ldap_password_func', + '--FILEDESC', 'set hook to mutate ldapbindpassw',]) +endif + +ldap_password_func = shared_module('ldap_password_func', + ldap_password_func_sources, + kwargs: pg_mod_args + { + 'dependencies': [ldap, pg_mod_args['dependencies']], + }, +) +testprep_targets += ldap_password_func + +tests += { + 'name': 'ldap_password_func', + 'sd': meson.current_source_dir(), + 'bd': meson.current_build_dir(), + 'tap': { + 'tests': [ + 't/001_mutated_bindpasswd.pl', + ], + 'env': {'with_ldap': 'yes'} + }, +} diff --git a/src/test/modules/ldap_password_func/t/001_mutated_bindpasswd.pl b/src/test/modules/ldap_password_func/t/001_mutated_bindpasswd.pl new file mode 100644 index 0000000000..c9d90194a8 --- /dev/null +++ b/src/test/modules/ldap_password_func/t/001_mutated_bindpasswd.pl @@ -0,0 +1,220 @@ + +# Copyright (c) 2022, PostgreSQL Global Development Group + +use strict; +use warnings; +use File::Copy; +use PostgreSQL::Test::Utils; +use PostgreSQL::Test::Cluster; +use Test::More; + + +my ($slapd, $ldap_bin_dir, $ldap_schema_dir); + +$ldap_bin_dir = undef; # usually in PATH + +if ($ENV{with_ldap} ne 'yes') +{ + plan skip_all => 'LDAP not supported by this build'; +} +elsif ($^O eq 'darwin' && -d '/usr/local/opt/openldap') +{ + # typical paths for Homebrew + $slapd = '/usr/local/opt/openldap/libexec/slapd'; + $ldap_schema_dir = '/usr/local/etc/openldap/schema'; +} +elsif ($^O eq 'darwin' && -d '/opt/local/etc/openldap') +{ + # typical paths for MacPorts + $slapd = '/opt/local/libexec/slapd'; + $ldap_schema_dir = '/opt/local/etc/openldap/schema'; +} +elsif ($^O eq 'linux') +{ + $slapd = '/usr/sbin/slapd'; + $ldap_schema_dir = '/etc/ldap/schema' if -d '/etc/ldap/schema'; + $ldap_schema_dir = '/etc/openldap/schema' if -d '/etc/openldap/schema'; +} +elsif ($^O eq 'freebsd') +{ + $slapd = '/usr/local/libexec/slapd'; + $ldap_schema_dir = '/usr/local/etc/openldap/schema'; +} +elsif ($^O eq 'openbsd') +{ + $slapd = '/usr/local/libexec/slapd'; + $ldap_schema_dir = '/usr/local/share/examples/openldap/schema'; +} +else +{ + plan skip_all => "ldap tests not supported on $^O or dependencies not installed"; +} + +# make your own edits here +#$slapd = ''; +#$ldap_bin_dir = ''; +#$ldap_schema_dir = ''; + +$ENV{PATH} = "$ldap_bin_dir:$ENV{PATH}" if $ldap_bin_dir; + +my $ldap_datadir = "${PostgreSQL::Test::Utils::tmp_check}/openldap-data"; +my $slapd_certs = "${PostgreSQL::Test::Utils::tmp_check}/slapd-certs"; +my $slapd_conf = "${PostgreSQL::Test::Utils::tmp_check}/slapd.conf"; +my $slapd_pidfile = "${PostgreSQL::Test::Utils::tmp_check}/slapd.pid"; +my $slapd_logfile = "${PostgreSQL::Test::Utils::log_path}/slapd.log"; +my $ldap_conf = "${PostgreSQL::Test::Utils::tmp_check}/ldap.conf"; +my $ldap_server = 'localhost'; +my $ldap_port = PostgreSQL::Test::Cluster::get_free_port(); +my $ldaps_port = PostgreSQL::Test::Cluster::get_free_port(); +my $ldap_url = "ldap://$ldap_server:$ldap_port"; +my $ldaps_url = "ldaps://$ldap_server:$ldaps_port"; +my $ldap_basedn = 'dc=example,dc=net'; +my $ldap_rootdn = 'cn=Manager,dc=example,dc=net'; +my $clear_ldap_rootpw = "FooBaR1"; +my $rot13_ldap_rootpw = "SbbOnE1"; +my $ldap_pwfile = "${PostgreSQL::Test::Utils::tmp_check}/ldappassword"; + +note "setting up slapd"; + +append_to_file( + $slapd_conf, + qq{include $ldap_schema_dir/core.schema +include $ldap_schema_dir/cosine.schema +include $ldap_schema_dir/nis.schema +include $ldap_schema_dir/inetorgperson.schema + +pidfile $slapd_pidfile +logfile $slapd_logfile + +access to * + by * read + by users auth + +database ldif +directory $ldap_datadir + +TLSCACertificateFile $slapd_certs/ca.crt +TLSCertificateFile $slapd_certs/server.crt +TLSCertificateKeyFile $slapd_certs/server.key + +suffix "dc=example,dc=net" +rootdn "$ldap_rootdn" +rootpw $clear_ldap_rootpw}); + +# don't bother to check the server's cert (though perhaps we should) +append_to_file( + $ldap_conf, + qq{TLS_REQCERT never +}); + +mkdir $ldap_datadir or die; +mkdir $slapd_certs or die; + +copy "../../ssl/ssl/server_ca.crt", "$slapd_certs/ca.crt" + || die "copying ca.crt: $!"; +copy "../../ssl/ssl/server-cn-only.crt", "$slapd_certs/server.crt" + || die "copying server.crt: $!";; +copy "../../ssl/ssl/server-cn-only.key", "$slapd_certs/server.key" + || die "copying server.key: $!";; + +system_or_bail $slapd, '-f', $slapd_conf, '-h', "$ldap_url $ldaps_url"; + +END +{ + kill 'INT', `cat $slapd_pidfile` if -f $slapd_pidfile; +} + +append_to_file($ldap_pwfile, $clear_ldap_rootpw); +chmod 0600, $ldap_pwfile or die; + +# wait until slapd accepts requests +my $retries = 0; +while (1) +{ + last + if ( + system_log( + "ldapsearch", "-sbase", + "-H", $ldap_url, + "-b", $ldap_basedn, + "-D", $ldap_rootdn, + "-y", $ldap_pwfile, + "-n", "'objectclass=*'") == 0); + die "cannot connect to slapd" if ++$retries >= 300; + note "waiting for slapd to accept requests..."; + Time::HiRes::usleep(1000000); +} + +$ENV{'LDAPURI'} = $ldap_url; +$ENV{'LDAPBINDDN'} = $ldap_rootdn; +$ENV{'LDAPCONF'} = $ldap_conf; + +note "loading LDAP data"; + +system_or_bail 'ldapadd', '-x', '-y', $ldap_pwfile, '-f', 'authdata.ldif'; +system_or_bail 'ldappasswd', '-x', '-y', $ldap_pwfile, '-s', 'secret1', + 'uid=test1,dc=example,dc=net'; +system_or_bail 'ldappasswd', '-x', '-y', $ldap_pwfile, '-s', 'secret2', + 'uid=test2,dc=example,dc=net'; + +note "setting up PostgreSQL instance"; + +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->append_conf('postgresql.conf', "log_connections = on\n"); +$node->append_conf('postgresql.conf', "shared_preload_libraries = 'ldap_password_func'"); +$node->start; + +$node->safe_psql('postgres', 'CREATE USER test0;'); +$node->safe_psql('postgres', 'CREATE USER test1;'); +$node->safe_psql('postgres', 'CREATE USER "te...@example.net";'); + +note "running tests"; + +sub test_access +{ + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my ($node, $role, $expected_res, $test_name, %params) = @_; + my $connstr = "user=$role"; + + if ($expected_res eq 0) + { + $node->connect_ok($connstr, $test_name, %params); + } + else + { + # No checks of the error message, only the status code. + $node->connect_fails($connstr, $test_name, %params); + } +} + +note "use ldapbindpasswd"; + +$ENV{"PGPASSWORD"} = 'secret1'; + +unlink($node->data_dir . '/pg_hba.conf'); +$node->append_conf('pg_hba.conf', + qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapbasedn="$ldap_basedn" ldapbinddn="$ldap_rootdn" ldapbindpasswd=wrong} +); +$node->restart; + +test_access($node, 'test1', 2, 'search+bind authentication fails with wrong ldapbindpasswd'); + +unlink($node->data_dir . '/pg_hba.conf'); +$node->append_conf('pg_hba.conf', + qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapbasedn="$ldap_basedn" ldapbinddn="$ldap_rootdn" ldapbindpasswd="$clear_ldap_rootpw"} +); +$node->restart; + +test_access($node, 'test1', 2, 'search+bind authentication fails with clear password'); + +unlink($node->data_dir . '/pg_hba.conf'); +$node->append_conf('pg_hba.conf', + qq{local all all ldap ldapserver=$ldap_server ldapport=$ldap_port ldapbasedn="$ldap_basedn" ldapbinddn="$ldap_rootdn" ldapbindpasswd="$rot13_ldap_rootpw"} +); +$node->restart; + +test_access($node, 'test1', 0, 'search+bind authentication succeeds with rot13ed password'); + +done_testing(); diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index 911a768a29..b797afe4f2 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -3,6 +3,7 @@ subdir('commit_ts') subdir('delay_execution') subdir('dummy_index_am') subdir('dummy_seclabel') +subdir('ldap_password_func') subdir('libpq_pipeline') subdir('plsample') subdir('snapshot_too_old') -- 2.34.1