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

Reply via email to