Add a hook for modifying the ldapbind password
authorAndrew Dunstan <andrew@dunslane.net>
Wed, 15 Mar 2023 20:37:28 +0000 (16:37 -0400)
committerAndrew Dunstan <andrew@dunslane.net>
Wed, 15 Mar 2023 20:37:28 +0000 (16:37 -0400)
The hook can be installed by a shared_preload library.

A similar mechanism could be used for radius paswords, for example, and
the type name auth_password_hook_typ has been shosen with that in mind.

John Naylor and Andrew Dunstan

Discussion: https://postgr.es/m/469b06ed-69de-ba59-c13a-91d2372e52a9@dunslane.net

src/backend/libpq/auth.c
src/include/libpq/auth.h
src/test/modules/Makefile
src/test/modules/ldap_password_func/Makefile [new file with mode: 0644]
src/test/modules/ldap_password_func/ldap_password_func.c [new file with mode: 0644]
src/test/modules/ldap_password_func/meson.build [new file with mode: 0644]
src/test/modules/ldap_password_func/t/001_mutated_bindpasswd.pl [new file with mode: 0644]
src/test/modules/meson.build

index 25b3a781cdc324786ec48a7f2bbcc6c2fc521cd3..bc0cf26b122a1b28c20fe037ec851c0e99b1ffb6 100644 (file)
@@ -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,
index 137bee7c45528c1acf67fae08d062268f8fb95df..9916c99df170369c547e81e5cca6d7a412b3984b 100644 (file)
@@ -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 */
index c629cbe383025568983219b423616f493b2fa3c1..79e3033ec284b9b040853fd69c7d8d61cad20c3f 100644 (file)
@@ -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 (file)
index 0000000..3324e04
--- /dev/null
@@ -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/ldap_password_func.c b/src/test/modules/ldap_password_func/ldap_password_func.c
new file mode 100644 (file)
index 0000000..4d980d2
--- /dev/null
@@ -0,0 +1,65 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 (file)
index 0000000..653a5e9
--- /dev/null
@@ -0,0 +1,33 @@
+if not ldap.found()
+  subdir_done()
+endif
+
+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']],
+  },
+)
+test_install_libs += 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 (file)
index 0000000..4174292
--- /dev/null
@@ -0,0 +1,103 @@
+
+# Copyright (c) 2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use File::Copy;
+use FindBin;
+use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Cluster;
+use Test::More;
+
+use lib "$FindBin::RealBin/../../../ldap";
+use LdapServer;
+
+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 ($ENV{PG_TEST_EXTRA} !~ /\bldap\b/)
+{
+       plan skip_all =>
+         'Potentially unsafe test LDAP not enabled in PG_TEST_EXTRA';
+}
+elsif (!$LdapServer::setup)
+{
+       plan skip_all =>
+         "ldap tests not supported on $^O or dependencies not installed";
+}
+
+my $clear_ldap_rootpw = "FooBaR1";
+my $rot13_ldap_rootpw = "SbbOnE1";
+
+my $ldap = LdapServer->new($clear_ldap_rootpw, 'users');    # no anonymous auth
+$ldap->ldapadd_file("$FindBin::RealBin/../../../ldap/authdata.ldif");
+$ldap->ldapsetpw('uid=test1,dc=example,dc=net', 'secret1');
+
+my ($ldap_server, $ldap_port, $ldap_basedn, $ldap_rootdn) =
+  $ldap->prop(qw(server port basedn rootdn));
+
+
+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 test1;');
+
+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();
index 1baa6b558d1c8156277ab0b779511742c9932dd7..dcb82ed68f481b02ac6fe43ff3cec5b9ff8b1082 100644 (file)
@@ -5,6 +5,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')